Skip to content

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:

  1. 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).
  2. 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.

POST /v1/agent/sign-up

Unauthenticated. IP-rate-limited at 5 requests per 60 seconds. Domain-velocity limited at 10 signups per 60 minutes per normalized email domain.

| 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 |

Terminal window
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"
}'
{
"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.

{
"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.

{
"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.

POST /v1/agent/verify

Authenticated with the restricted live key returned by sign-up. Body: { otp } — 6 numeric digits.

| Field | Type | Required | Description | |-------|------|----------|-------------| | otp | string | yes | Six-digit numeric code from the verification email |

Terminal window
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"}'
{
"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.

{
"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.

Agent self-signup uses two internal plan tiers (free-agent-unverifiedfree-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.