Agent Auth
Agent self-signup lets an AI agent provision its own Chronary organization and API key without a human in the loop, then upgrade to a larger budget by verifying a one-time code sent to the signup email.
The flow is two requests:
POST /v1/agent/sign-up— unauthenticated. Creates the org (or dedups) and emails a 6-digit code. New orgs get an API key on the sandbox tier (small but functional — see Sandbox tier vs. Free tier below).POST /v1/agent/verify— authenticated with the sandbox key. Submits the OTP to lift caps to the public Free tier.
The same api_key value is used before and after verification; only the org’s plan and status change.
Sign up
Section titled “Sign up”POST /v1/agent/sign-upUnauthenticated. IP-rate-limited at 5 requests per 60 seconds. Domain-velocity limited at 10 signups per 60 minutes per normalized email domain.
Request body
Section titled “Request body”| Field | Type | Required | Description |
|-------|------|----------|-------------|
| email | string | yes | Email address to send the verification code to |
| agent_name | string | yes | Display name of the signing-up agent (1–100 chars) |
| tos_version | string | yes | Exact ToS version string the caller has accepted |
Example
Section titled “Example”curl -X POST https://api.chronary.ai/v1/agent/sign-up \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "agent_name": "Alice Bot", "tos_version": "2026-04-17" }'import { Chronary, isAgentSignUpNewOrg } from '@chronary/sdk';
// Construct without an API key — sign-up is unauthenticated.const client = new Chronary();
const result = await client.agentAuth.signUp({ agent_name: 'Alice Bot', tos_version: '2026-04-17',});
if (isAgentSignUpNewOrg(result)) { console.log('Restricted key:', result.api_key);} else { console.log(result.message); // "Verification code sent to email"}from chronary import Chronary
# Public endpoint — construct without an api_key.client = Chronary(api_key=None)
result = client.agent_auth.sign_up( agent_name="Alice Bot", tos_version="2026-04-17",)
if result.is_new_org: print("Restricted key:", result.api_key)else: print(result.message)chronary auth signup \ --agent-name "Alice Bot" \ --tos-version 2026-04-17Response 200 OK — new org
Section titled “Response 200 OK — new org”{ "org_id": "org_abc123", "agent_id": "agt_abc123", "api_key": "chr_sk_restricted_abc...", "message": "Verification code sent to email"}The api_key runs on the sandbox tier (free-agent-unverified). It can call every Free-tier endpoint the agent is going to use — calendars, events, webhooks, iCal, availability — but on a small monthly budget (10 events, 25 webhook deliveries, 1 webhook endpoint, 1 iCal subscription, 50 availability queries, 1,000 API calls). Once any cap is hit you’ll get a quota_exceeded response; verifying the email lifts each cap to the public Free-tier value. Pro-only endpoints (/v1/scheduling, /v1/keys, cross-calendar availability, temporal holds) remain capability-gated and require a paid plan regardless of verification status.
Response 200 OK — existing-org dedup
Section titled “Response 200 OK — existing-org dedup”{ "message": "Verification code sent to email"}To prevent email enumeration, the same opaque message is returned whether or not an org already exists for that email. No credentials are leaked.
Response 409 Conflict — stale ToS
Section titled “Response 409 Conflict — stale ToS”{ "error": { "type": "tos_version_stale", "message": "The submitted terms-of-service version is out of date", "current_version": "2026-05-01", "request_id": "req_..." }}Re-fetch the current ToS version from GET /v1/terms and retry.
Verify OTP
Section titled “Verify OTP”POST /v1/agent/verifyAuthenticated with the restricted live key returned by sign-up. Body: { otp } — 6 numeric digits.
Request body
Section titled “Request body”| Field | Type | Required | Description |
|-------|------|----------|-------------|
| otp | string | yes | Six-digit numeric code from the verification email |
Example
Section titled “Example”curl -X POST https://api.chronary.ai/v1/agent/verify \ -H "Authorization: Bearer chr_sk_restricted_abc..." \ -H "Content-Type: application/json" \ -d '{"otp": "123456"}'import { Chronary } from '@chronary/sdk';
// Use the restricted key from the sign-up response.const client = new Chronary({ apiKey: signUpResult.api_key });
const { verified } = await client.agentAuth.verify({ otp: '123456' });from chronary import Chronary
client = Chronary(api_key=sign_up_result.api_key)
result = client.agent_auth.verify(otp="123456")assert result.verified is Truechronary auth verify \ --otp 123456 \ --api-key chr_sk_restricted_abc...Response 200 OK
Section titled “Response 200 OK”{ "verified": true, "message": "Full access unlocked"}After a successful verify, the org transitions status: unverified → verified, the plan upgrades from free-agent-unverified to free-agent, and the same api_key gains full write access.
Response 400 Bad Request
Section titled “Response 400 Bad Request”{ "error": { "type": "validation_error", "message": "Invalid or expired verification code", "request_id": "req_..." }}A generic 400 is returned for every failure mode (wrong OTP, expired, burned after 10 attempts) — no information leak between those cases. Re-call POST /v1/agent/sign-up with the same email to get a fresh code.
Sandbox tier vs. Free tier
Section titled “Sandbox tier vs. Free tier”Agent self-signup uses two internal plan tiers (free-agent-unverified → free-agent) that aren’t returned by GET /v1/plans because they’re enforcement states, not purchasable products. The pre-verification tier is a sandbox — small but functional caps so the operator can wire the API end-to-end before committing to verification. The verified tier matches the public free tier exactly, enforced identically at every quota gate.
| Cap | Sandbox (pre-verify) | Verified (= Free tier) | Verify gain | |---|---|---|---| | Agents | 1 | 3 | 3× | | Calendars | 1 | 10 | 10× | | Events / mo | 10 | 2,500 | 250× | | API calls / mo | 1,000 | 50,000 | 50× | | Availability queries / mo | 50 | 10,000 | 200× | | Webhook deliveries / mo | 25 | 5,000 | 200× | | Webhook endpoints | 1 | 3 | 3× | | iCal subscriptions | 1 | 5 | 5× | | Scheduling proposals | 0 (Pro feature) | 0 (Pro feature) | — | | Scoped API keys | 0 (Pro feature) | 0 (Pro feature) | — |
The sandbox is sized to let an agent create real events, see a webhook fire, subscribe to its own iCal feed, and run a handful of availability queries — but not enough to run a real workload. Once you hit a cap, you’ll get a quota_exceeded response (HTTP 429) and need to verify the email to keep going.
Per-resource quotas (events, webhook deliveries, availability queries, proposals) are enforced for every free-tier plan variant — free, free-agent, and free-agent-unverified — via a single isFreeTier(plan) predicate at each service gate. There is no enforcement carve-out for agent-signed-up orgs; the sandbox caps you see above are the caps you’ll hit. api_calls is metered for every plan via the request-level quotas middleware and behaves identically across all tiers.
After verification, the same api_key value is reused (no rotation needed); only the status and plan columns on the org transition, and the caps immediately jump to the Free-tier values above. Any work created during the sandbox window (calendars, events, webhooks, iCal subs) carries over — verification doesn’t reset state.