Account API reference

Operations for managing an account itself: bootstrapping one programmatically, checking your send budget and usage, turning on optional scanning, exporting your data, and deleting the account. Dashboard-only account settings (billing checkout, inbox counts) are managed from the web dashboard and are not part of this API surface.

For the machine-readable contract, see the generated OpenAPI spec at /docs/openapi.json. Error codes and their remedies are catalogued once at /agents/errors — this page names the codes each operation can return and links there instead of restating remediation.

Auth is either a Bearer API key (admin or agent role) or a dashboard session cookie; where an operation is admin-only, an agent-role key is rejected with 403 INSUFFICIENT_SCOPE. Each operation below carries a Classification tag drawn from read-only, mutating, and secret-revealing.

POST /v1/accounts/signup

Create a new account with an API key and no password — the programmatic/CLI signup path. Every new account must verify both its email address and a mobile phone number before protected product routes unlock.

Auth: none (public endpoint). Classification: mutating, secret-revealing.

Request:

{
  "email": "[email protected]",
  "phone_number": "+13125550123",
  "accept_terms": true,
  "invite_code": "...",
  "cli_signup_code": "...",
  "accept_web_risk_version": "v2"
}
  • phone_number is required and must include its country code. Common spaces, parentheses, periods, and hyphens are normalized before validation and storage in E.164 form. ReplyLayer sends a 6-digit SMS one-time password through Telnyx Verify v2; signup responses expose only the masked destination.
  • accept_terms must be true; it acknowledges the Terms of Service, Privacy Policy, Acceptable Use Policy, and DPA.
  • Depending on how signup is gated for your deployment, the request must carry either an invite_code or a single-use cli_signup_code (issued to you from the dashboard by an existing customer). A missing code returns 403 CLI_SIGNUP_CODE_REQUIRED; an invalid, expired, or already-consumed code returns 403 CLI_SIGNUP_CODE_INVALID.
  • accept_web_risk_version is optional and acknowledges the malicious-link- scanning (URL reputation) disclosure. Its absence creates the account normally with link scanning left disabled — you can enable it later via POST /v1/accounts/url-reputation. Mismatched values are treated the same as absence.

Response (201):

{
  "account_id": "uuid",
  "api_key": "rly_live_<public_id>.<secret>",
  "verification_required": true,
  "email_verification_required": true,
  "phone_verification_required": true,
  "sms_delivery_status": "sent",
  "phone_number_masked": "•••• 0123",
  "message": "Check your email for a 6-digit verification code."
}

The api_key is shown in plaintext only on this response — store it immediately. It can call the verification/recovery endpoints, but protected product routes return 403 EMAIL_NOT_VERIFIED while email is pending and then 403 PHONE_NOT_VERIFIED while phone verification is pending. sms_delivery_status is sent, pending, or not_required; pending means account creation succeeded but the caller must retry the SMS with POST /v1/auth/resend-phone-verification. See /docs/authentication for both verification steps.

Errors you'll care about: 403 CLI_SIGNUP_CODE_REQUIRED / 403 CLI_SIGNUP_CODE_INVALID (signup gate), and 429 SIGNUP_RATE_LIMITED — a per-IP limiter caps this endpoint at 10 requests / 60s and returns a Retry-After header plus details.retry_after, distinct from the generic RATE_LIMITED.

GET /v1/accounts

Minimal identity ("whoami") for the authenticated caller.

Auth: any account-scoped key (admin or agent) or session. Classification: read-only.

Response (200):

{ "account_id": "uuid", "email": "[email protected]", "status": "active", "tier": "sandbox" }

Useful for confirming which account a key belongs to — for example, echoing the account's own email into the confirm_email field required by account deletion, without persisting the email locally.

GET /v1/accounts/quota

Send-budget preflight — the effective daily send limit and how much of it remains today. This is the endpoint an agent should call before a send.

Auth: admin, agent, or session (all receive a 200 — unlike GET /v1/accounts/usage, which is admin-only). Classification: read-only.

Response (200):

{
  "today": { "count": 5, "limit": 200, "day": "2026-05-30" },
  "sends_remaining": 195,
  "reset_at": "2026-05-31T00:00:00.000Z",
  "scope": "agent",
  "bound_mailbox_ids": ["uuid1", "uuid2"]
}
  • today.limit is the effective daily limit — it accounts for trust level and may be lower than the raw tier cap. A sandbox account reports 15 here. This is the same value the send-time budget gate enforces, so sends_remaining = max(0, limit - count) matches what a send will actually allow. See /docs/limits for the full tier and sandbox table.
  • reset_at is an ISO-8601 UTC instant at midnight UTC of the next UTC day, when the budget resets.
  • scope disambiguates the two empty-bound_mailbox_ids cases: "admin" (an unrestricted key/session — [] means all mailboxes) vs "agent" (a scoped key — an empty list means the key has no bound mailboxes and cannot send). Key off scope, not bound_mailbox_ids.length, to tell "all" from "none".
  • An optional warmup object appears only while a new paid account is inside its shared-domain new-sender warm-up window; when present, today.limit reflects the warm-up cap so the preflight never advertises headroom a send would refuse.

When the budget is exhausted, send endpoints return 429 RATE_LIMITED with enriched details: { daily_limit, sends_remaining: 0, reset_at } — the same numbers reported here. See /agents/send-gates.

GET /v1/accounts/usage

Full account usage: send budget, 30-day history, mailbox count, deliverability rates, health status bands, and trust-promotion progress.

Auth: admin or session only (agent keys → 403 INSUFFICIENT_SCOPE). Classification: read-only.

Response (200):

{
  "today": { "count": 5, "limit": 100, "day": "2026-04-08" },
  "history": [{ "day": "2026-04-07", "count": 12 }],
  "mailbox_count": 2,
  "mailbox_limit": 5,
  "rates": { "bounce_7d": 0.012, "complaint_7d": 0.0003, "delivery_7d": 0.987 },
  "health": {
    "bounce": {
      "value": 0.012,
      "status": "healthy",
      "basis_count": 84,
      "window": "7d",
      "summary": "Bounce rate is comfortably within acceptable range.",
      "action_hint": null
    }
  },
  "trust": {
    "level": 1,
    "next_promotion": {
      "target_level": 2,
      "eligible": false,
      "requirements": {
        "min_age_days": { "required": 7, "current": 3, "met": false },
        "min_emails": { "required": 20, "current": 12, "met": false },
        "max_bounce_rate": { "required": 0.02, "current": 0.012, "met": true },
        "max_complaint_rate": { "required": 0.0005, "current": 0.0003, "met": true }
      }
    }
  }
}
  • rates are cohort-based, windowed on provider-accepted sends.
  • health gives server-derived display bands: status is one of healthy, watch, at_risk, or insufficient_data. Enforcement thresholds are not exposed.
  • next_promotion is null at the lowest trust level (which requires payment to advance) and at the maximum level.

POST /v1/accounts/url-reputation

Enable malicious link scanning (URL reputation) for the account, recording acknowledgement of the current disclosure. This turns on an account-wide scanning data flow, so it is an admin action.

Auth: admin Bearer or session only (agent keys → 403 INSUFFICIENT_SCOPE, before any work). Classification: mutating.

Request:

{ "accept_web_risk_version": "v2" }

accept_web_risk_version is required and must equal the current disclosure version. Read that version from the account-info endpoint (GET /v1/auth/me), which reports a url_reputation block (active, accepted_version, current_version, privacy_ok). A stale value returns 400 DISCLAIMER_VERSION_MISMATCH; if the account's accepted privacy version predates the disclosed sub-processor, the call returns 409 PRIVACY_VERSION_TOO_OLD — re-accept the current Privacy Policy first.

Response (200):

{
  "url_reputation": {
    "active": true,
    "accepted_version": "v2",
    "current_version": "v2",
    "privacy_ok": true
  },
  "disclosure": { "notice": "<disclosure text>", "advisory_url": "<advisory link>" }
}

The disclosure object echoes the notice text and an advisory link for the underlying reputation-data provider named in the Privacy Policy.

GET /v1/accounts/export

Data portability export (GDPR Art. 20): all account data as structured JSON.

Auth: admin or session only (agent keys → 403 INSUFFICIENT_SCOPE). Classification: read-only.

Response (200):

{
  "account": { "id": "uuid", "email": "...", "phone_number": "+13125550123", "phone_verification_required_at": "...", "phone_verified_at": "...", "tier": "sandbox", "status": "active", "data_region": "us" },
  "mailboxes": [],
  "messages": { "items": [], "total": 89, "truncated": false },
  "message_events": { "items": [], "truncated": false },
  "recipients": [],
  "domains": [],
  "api_keys": [{ "id": "uuid", "prefix": "rly_live_<public_id>.****", "status": "active" }],
  "send_budgets": [],
  "trust_history": [],
  "suppressed_addresses": [],
  "exported_at": "2026-04-09T00:00:00.000Z"
}

Messages and events are capped at 10,000 items, with truncated: true when more exist. API keys are masked — the key hash is never exposed. Each successful export writes one audit entry with counts only (no PII).

DELETE /v1/accounts · DELETE /v1/accounts/:id

Soft-delete the account with a 30-day grace period. Both forms have identical semantics; the :id variant exists for REST-idiomatic callers (the :id must equal the caller's own account id).

Auth: admin Bearer or session (agent keys → 403 INSUFFICIENT_SCOPE). Classification: mutating (destructive).

Two layers of confirmation:

  1. Intent gate (all paths, Bearer included). The body must include confirm_email equal to the account's own email (case-insensitive). Missing → 400 DELETE_CONFIRMATION_REQUIRED; wrong email → 403 DELETE_CONFIRMATION_MISMATCH.
  2. Auth factor. A Bearer admin key skips step-up (possession of the key is the confirmation). A dashboard session runs a method-aware step-up ladder: for an SMS-based MFA account, an SMS re-auth code — mint a challenge with POST /v1/auth/reauth/sms/send (action_scope: "account_delete"), then pass reauth_challenge + mfa_code; for a TOTP account, a totp_code; else the account password; else — for an SSO-only account with none — 400 DELETE_REQUIRES_TOTP (with details.required_method: "totp_or_sms"). mfa_code is the method-blind field for the SMS code; totp_code is still accepted as its alias. Exception: an account that never completed signup phone verification (and has no step-up factor) deletes with confirm_email alone, since MFA enrollment is blocked until the phone is verified and such an account cannot hold resources.

Request (Bearer path):

{ "confirm_email": "[email protected]" }

Request (session path — TOTP account):

{ "confirm_email": "[email protected]", "password": "..." }

Request (session path — SMS-MFA account):

{ "confirm_email": "[email protected]", "reauth_challenge": "uuid", "mfa_code": "123456" }

Response (200):

{
  "status": "deleted",
  "message": "Account scheduled for deletion. You have 30 days to contact support to reinstate."
}

Hard purge runs automatically after 30 days: it removes database rows, stored message contents, and any custom-domain DNS configuration; an anonymized audit trail is retained and a confirmation email is sent before purge.

Errors you'll care about:

  • 409 LEGAL_HOLD_ACTIVE — the account is under a customer legal hold; lift it first.
  • 503 BILLING_TEARDOWN_RETRY — billing teardown failed transiently; the account stays active and the request can be retried.
  • On the :id form only: 400 VALIDATION_ERROR when :id is not a UUID, and 403 INSUFFICIENT_SCOPE when :id does not match the caller's account id.