Account API reference

Operations for managing an account itself: bootstrapping one programmatically, checking your send budget and usage, managing link-scanning activation, 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 records the single signup legal-assent action: agreement to the Terms of Service — which incorporate the Acceptable Use Policy and, where applicable, the Data Processing Agreement — and acknowledgement of the Privacy Policy. The field name is retained for API compatibility.
  • 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 records an explicit acknowledgement of the malicious-link-scanning (URL reputation) disclosure. Its absence enables link scanning by default under the signup disclosure (Privacy Policy §7a). A mismatched (stale) explicit value creates the account normally with link scanning disabled — enable it later via POST /v1/accounts/url-reputation. You can turn scanning off per mailbox via the mailbox scanner policy (disabled_scanners: ["url-reputation"]).

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. New accounts have link scanning enabled by default at signup, so this route mostly serves accounts that predate default-on or a disclaimer version bump. It 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 acknowledged privacy version predates the disclosed sub-processor, the call returns 409 PRIVACY_VERSION_TOO_OLD — review and acknowledge 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.