Authentication
Every programmatic request authenticates with a Bearer API key:
GET /v1/auth/me HTTP/1.1
Host: api.replylayer.ai
Authorization: Bearer rly_live_k3m9p2qx7vn4hjd0.uZ8Qb1vK3mN0pR7sT2wX9yA4cF6gH8jL1nP3rT5vW7zThe base URL is https://api.replylayer.ai/v1. All endpoints except the auth and
health routes require a key. Session cookies exist for the dashboard only — as an
agent you always use a Bearer key.
Key format
A key is a single opaque string in two dot-separated parts:
rly_live_<public_id>.<secret><public_id>— the 16-character head afterrly_live_(lowercased base32). It is not secret; it identifies the key for leak-attribution and appears (masked) in the key-list read. Do not authenticate with it alone.<secret>— the credential proper (the part after the.). The whole token, both parts together, is what authenticates.
Treat the entire rly_live_... string as the secret. The plaintext is returned
exactly once, on the create response — there is no endpoint that re-reveals it.
If you lose it, mint a replacement. A key using a retired prefix (anything other than
rly_live_) is rejected at the door with 401 API_KEY_FORMAT_LEGACY — rotate to a
current key.
Two roles: admin and agent
| Role | Scope | Can it manage keys / mailboxes / account? |
|---|---|---|
admin | Full account access — same reach as a dashboard session. | Yes. |
agent | Send, receive, read, reply, and quarantine-manage on its bound mailboxes only. | No — cannot create/delete mailboxes, mint or list keys, edit scanner policy, or touch account settings. |
An agent key is bound to one or more mailboxes at creation (mailbox_ids,
non-empty and required for role: "agent"; forbidden for role: "admin"). It can
never see or act on a mailbox outside that set.
Fail-to-zero scoping
Agent scope fails closed. If every mailbox an agent key was bound to is later
deleted, the key resolves to zero mailbox access — never to unrestricted access.
A scoped key that has lost all its bindings can authenticate but cannot send or read
anything; it does not silently widen to "all mailboxes". Deleted mailboxes also drop
out of the key's reported mailbox_ids / api_key_mailbox_ids.
Bearer-over-session precedence
If a request carries both an Authorization: Bearer header and an
rl_session cookie, the Bearer key wins and the request runs as that key — the cookie
is ignored. This matters most during rotation: a tool that quietly injects a stale
Authorization header (a copied browser cURL, an SDK holding an old key) will run the
Bearer branch even when you meant to act as your dashboard session. Send exactly one
credential per request.
Rotation: create → verify → revoke
The zero-downtime pattern for a running integration is three explicit steps. Both new and old keys are valid simultaneously between step 1 and step 3, so there is no window where your integration is un-authenticated.
1. Create a new key (does not touch existing keys). Admin-only:
POST /v1/accounts/api-keys
Authorization: Bearer <current-admin-key>
Content-Type: application/json
{ "role": "agent", "label": "support-bot", "mailbox_ids": ["<mailbox-uuid>"] }201 Created
{
"id": "<key-uuid>",
"api_key": "rly_live_<public_id>.<secret>",
"role": "agent",
"label": "support-bot",
"mailbox_ids": ["<mailbox-uuid>"]
}Save api_key now — it is shown only here. An account may hold at most 10 active
keys; at the cap the create call returns 403 FORBIDDEN, so revoke a stale key
first. To replace an agent key with the same reach, create a fresh agent key carrying
the same mailbox_ids.
2. Verify the new key against an authenticated read before you cut over. Use
GET /v1/auth/me (cheap, single response):
GET /v1/auth/me
Authorization: Bearer <new-key>200 OK
{ "account_id": "...", "email": "...", "tier": "...", "status": "active", "mfa_enabled": false, "email_verified": true, "phone_verification_required": true, "phone_verified": true, "phone_number_masked": "•••• 0123" }For a Bearer key the response also carries api_key_role ("admin" | "agent") and
api_key_mailbox_ids, so you can confirm the new key resolves to the role and mailbox
set you expect. Do not use GET /v1/health to validate a key — health is
auth-exempt and returns 200 regardless of whether the token is valid, revoked, or
absent.
3. Revoke the old key once traffic has moved:
DELETE /v1/accounts/api-keys/<old-key-id>
Authorization: Bearer <new-admin-key>200 OK
{ "status": "revoked" }Find <old-key-id> in the id field of the key list (GET /v1/accounts/api-keys).
DELETE on a non-existent or already-revoked key returns 404 NOT_FOUND. Revocation is
not cached — the revoked key's very next request fails auth.
The rotate endpoint (destructive, admin-only)
POST /v1/accounts/api-keys/rotate mints a new admin key and revokes old admin
key(s) in the same transaction — there is no verify window, so any in-flight request
that lands between the commit and your secret-store update fails with 401. Its blast
radius depends on the caller:
- Bearer (API key) — revokes only the calling key, mints one new admin key.
- Session (dashboard) — revokes all admin keys, mints one new admin key.
Rotate never touches agent keys. Prefer the create → verify → revoke sequence above for anything you can't afford to 401; use rotate only for a personal session or a compromise sweep where absorbing in-flight 401s is acceptable.
Self-revoke
Any key — including an agent key — may revoke itself by passing its own id to
DELETE /v1/accounts/api-keys/:id. This lets a compromised or retiring agent burn its
own credential without an admin in the loop; it is strictly capability-reducing. After a
successful self-revoke, that key's next request returns 401.
Revoking any other key still requires an admin key or a dashboard session. An agent
key that targets a different key's id gets 403 INSUFFICIENT_SCOPE.
Per-key instruction-trust capability (read-only)
Each key exposes a boolean instruction_trust_enabled (visible in
GET /v1/accounts/api-keys). It is off by default and is a read-path capability
only — it can, under the right conditions, relax the agent_safety_context guidance
an inbound read returns for a granted, verified sender. It grants no send or write
power. See Trusted instruction sources for exactly when a
read relaxes and how to branch on it.
Two hard rules for an agent integration:
- An agent cannot enable this for itself. Turning
instruction_trust_enabledon is a loosening action, restricted to a dashboard session with fresh re-authentication. A Bearer key — admin or agent — that calls the enable route is rejected with403 REAUTH_REQUIRES_SESSION. There is no client-sent opt-in; the relaxation is applied server-side by operator configuration only. - Turning it off is unprivileged. Disabling the capability is a tightening action and is agent-callable, so an agent can drop its own relaxed reads instantly for incident response.
The capability is also per-key and non-transferable: a freshly created or rotated key
always starts instruction_trust_enabled: false, regardless of what the key it replaced
had. Re-enable it deliberately (session + re-auth) if a replacement key needs it. Until it
is enabled, reads by that key fall back to the standard untrusted-content guidance with no
error surfaced.
Scope and auth error codes
These are the codes that signal an auth or scope problem. The full, curated error catalog
lives at /agents/errors — branch on code, not on the human error
string.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Missing, malformed, invalid, or revoked Bearer key. |
| 401 | API_KEY_FORMAT_LEGACY | Key uses a retired prefix — rotate to a current rly_live_ key. |
| 403 | INSUFFICIENT_SCOPE | An agent key hit an admin-only route (or tried to revoke another key). |
| 403 | MAILBOX_ACCESS_DENIED | An agent key acted on a mailbox it is not bound to (collection reads/writes). |
| 404 | NOT_FOUND | Single-resource detail read of a resource the key can't reach (masked to 404 so existence doesn't leak), or DELETE of an already-revoked/missing key. |
| 403 | REAUTH_REQUIRES_SESSION | A Bearer key hit a route that is session-only + fresh re-auth — enabling instruction trust, the loosening attachment gates, or any MFA-management / SMS step-up ceremony. |
| 403 | ACCOUNT_SUSPENDED | The account is not active. |
| 403 | EMAIL_NOT_VERIFIED | The account's email is unverified — most routes stay gated until it is. |
| 403 | PHONE_NOT_VERIFIED | The required signup phone is unverified — protected routes stay gated until the SMS check completes. |
Note the masking asymmetry for agent keys reaching outside their scope: a detail read
(GET /v1/messages/:id, GET /v1/mailboxes/:id) returns 404 NOT_FOUND so it can't be
used to probe for the existence of mailboxes you can't see, while a collection read
(GET /v1/mailboxes/:id/messages and siblings) returns 403 MAILBOX_ACCESS_DENIED for an
unbound mailbox UUID (and 404 for an unbound mailbox name).
MFA is a human, session-only concern
Two-factor authentication (TOTP or SMS) protects the human dashboard session, not the
API-key surface. Do not attempt any MFA route with an API key. Enrolling, switching,
or disabling a factor, the SMS step-up used to re-authorize a loosening action, and the
lost-factor recovery and phone-change ceremonies are all dashboard-session-only — a Bearer
key (admin or agent) is rejected with 403 REAUTH_REQUIRES_SESSION before anything
happens. The full family of SMS-MFA and step-up codes (MFA_SMS_*, REAUTH_*_SMS_*,
MFA_RECOVERY_*, PRIVACY_ACK_REQUIRED) is catalogued for completeness at
/agents/errors, but an API key only ever sees REAUTH_REQUIRES_SESSION
from this surface.
Signup verification bootstrap
Every new password, API/CLI, Google, or GitHub account supplies a mobile number. Existing OAuth logins do not. Protected product routes and provisioning wait for both email and phone verification; gate precedence is email first, then phone. The initial API key/session remains valid for these recovery routes while gated:
POST /v1/auth/verify-email
Authorization: Bearer <new-key>
Content-Type: application/json
{ "code": "482917" }POST /v1/auth/verify-phone
Authorization: Bearer <new-key>
Content-Type: application/json
{ "code": "593104" }SMS codes contain six digits, expire after 5 minutes, and only the newest
challenge is valid. Resend with an empty object to
POST /v1/auth/resend-phone-verification; before verification, the same endpoint
also accepts { "phone_number": "+13125550124" } to correct a typo and send a
fresh code. Treat 429 PHONE_VERIFICATION_RATE_LIMITED as a wait instruction and
honor Retry-After. GET /v1/auth/me returns only phone_number_masked, never the
full number.
See also
- Messages and state — the message lifecycle and verdict vocabulary.
- Security model — the untrusted-content contract for inbound bodies, attachments, and links.
- Send gates — why an agent-origin send may be additionally contained.
- Errors — the full error-code catalog and denial envelope.