{"openapi":"3.1.0","info":{"title":"EmailPal API","version":"1.0.0","description":"Provision sending infrastructure — domains, mailboxes, warming and inbox — from code. Anything that touches a registrar, a resolver or a mail server answers 202 with a job to poll rather than holding the connection open.","contact":{"name":"EmailPal support","url":"https://emailpal.io/docs"},"termsOfService":"https://emailpal.io/legal/terms"},"servers":[{"url":"https://emailpal.io/api/public/v1","description":"Production"}],"tags":[{"name":"Account","description":"Plan, usage and the prepaid balance domains are bought from. Worth calling first and then rarely."},{"name":"Domains","description":"Search, buy, connect one you already own, and manage its DNS. Buying spends real money from the domain balance."},{"name":"Mailboxes","description":"Create them by the hundred, adjust them, and export SMTP and IMAP credentials when you are ready to send."},{"name":"Warming","description":"Ramp progress, measured placement, and the switches that decide what warming costs."},{"name":"Inbox","description":"Read and send from any mailbox. Warming traffic is excluded by default and should stay that way."},{"name":"Jobs","description":"Where every 202 ends up. Poll these rather than assuming an accepted request succeeded."},{"name":"Compliance","description":"Check a list before you send to it, and keep addresses that must never be mailed out of your campaigns."}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key from the dashboard, sent as `Authorization: Bearer ep_live_…`. Scopes are hierarchical: `admin` implies `write` implies `read`."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Stable machine-readable identifier. Branch on this."},"message":{"type":"string","description":"Written for a human. Do not parse it."}},"additionalProperties":true}}}}},"paths":{"/account":{"get":{"operationId":"getAccount","summary":"Plan, usage, warming cost and domain balance","description":"Everything needed to know whether a create will succeed before attempting it. A 402 halfway through a batch of forty mailboxes leaves you working out which ones landed, so the intended pattern is to read this, size the batch, and then send it.\n\nIt also carries `domain_balance_cents`, which is the figure a domain purchase is charged against. That is a separate pot from the subscription card and has to cover the whole batch before any of it is bought.","tags":["Account"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"Always, for a valid key on a live account.","content":{"application/json":{"example":{"id":"acc_7f3d21","name":"Northworks","status":"active","enforcement":"none","created_at":"2026-02-14T09:12:44.000Z","plan":{"code":"growth","name":"Growth","monthly_price_cents":18900,"included_mailboxes":200,"included_domains":40,"mailbox_overage_cents":100,"warming_price_cents":60,"dedicated_ip_cents":2000,"prewarmed_inbox_cents":300,"domain_markup_cents":300,"daily_send_per_mailbox":40,"api_requests_per_hour":30000},"usage":{"mailboxes":{"used":148,"limit":200},"domains":{"used":26,"limit":40},"daily_send":{"used":4210,"limit":8000},"warming_mailboxes":96,"warming_monthly_cost_cents":5760},"domain_balance_cents":24150,"top_up_url":"/api/public/v1/account/topup"}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`account_missing` — The account could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/account/topup":{"post":{"operationId":"topup","summary":"Add money to the domain balance","description":"Returns a hosted payment page. Nothing is charged by this call: the balance goes up when the processor confirms the payment, which is usually a second or two after somebody completes that page.\n\nThis exists so an integration that hits `insufficient_balance` on a purchase has somewhere to go. Without it the automation ends in an email to a human who then has to find the dashboard.\n\n`admin` scope rather than `write`, because spending money is not the same permission as creating a mailbox, and a key handed to a provisioning script should not be able to run up a card.","tags":["Account"],"security":[{"bearerAuth":[]}],"x-required-scope":"admin","responses":{"201":{"description":"A payment page was created. No money has moved yet.","content":{"application/json":{"example":{"payment_url":"https://checkout.stripe.com/c/pay/cs_live_a1b2c3","reference":"cs_live_a1b2c3","amount_cents":25000,"balance_cents":24150,"note":"Nothing has been charged yet. Open the payment URL to pay; the balance goes up when the processor confirms it, which is usually within a few seconds of that page completing."}}}},"400":{"description":"`invalid_request` — `return_url` is not `https://`.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`account_missing` — The account could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`billing_provider_error` — The payment processor refused to open a session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"amount_cents":{"type":"integer","description":"Between $20 and $5,000. The floor is there because a payment smaller than it costs more in processor fees than it credits; the ceiling catches a mistyped amount before it becomes a refund.","minimum":2000,"maximum":500000},"return_url":{"type":"string","description":"Where the payer lands afterwards. Must be `https://`. We append `topup=complete` or `topup=cancelled` so the page can say which happened.","maxLength":500}},"required":["amount_cents"],"additionalProperties":false},"example":{"amount_cents":25000,"return_url":"https://your-app.example/billing"}}}}}},"/domains":{"get":{"operationId":"listDomains","summary":"List domains","description":"Newest first. Filter by status to answer a specific question: `needs_dns` for domains waiting on somebody to delegate nameservers, `aging` for inventory not yet old enough to send from, `ready` and `active` for the ones that can take mailboxes.","tags":["Domains"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"Always, including when the account has no domains.","content":{"application/json":{"example":{"data":[{"id":"dom_91af22","name":"outbound-atlas.com","status":"active","customer_owned":false,"nameservers_verified":true,"registered_at":"2026-03-02T11:00:00.000Z","aging_started_at":"2026-03-02T11:00:00.000Z","expires_at":"2027-03-02T11:00:00.000Z","auto_renew":true,"reputation_score":94,"mailbox_count":6,"daily_capacity":240,"registrar":"dynadot","created_at":"2026-03-02T10:58:11.000Z"}],"has_more":false,"next_cursor":null}}}},"400":{"description":"`invalid_status` — `status` is not one of the listed values.\n`invalid_pagination` — `limit` is out of range or `cursor` is not one we issued.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`read_failed` — The query failed.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"How many to return.","minimum":1,"maximum":200},"description":"How many to return."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","description":"The `next_cursor` from the previous page. Keyset rather than an offset, so rows created while you page do not shift the window and make you miss one.","maxLength":200},"description":"The `next_cursor` from the previous page. Keyset rather than an offset, so rows created while you page do not shift the window and make you miss one."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending_purchase","purchased","dns_pending","needs_dns","aging","ready","active","suspended","retired"],"description":"Only domains in this state."},"description":"Only domains in this state."}]},"post":{"operationId":"createDomains","summary":"Buy domains, or connect ones you own","description":"One endpoint with a `mode`, rather than two URLs. Buying and connecting produce the same kind of row and are genuinely different acts, and splitting them across two paths would let a client that meant to connect accidentally spend money at a registrar.\n\n**`mode: \"purchase\"`** prices the whole batch against the registrar, takes the total off the domain balance, and only then queues the registrations. If the balance will not cover all of it, nothing is bought and you get a 402 carrying the shortfall — a partial charge against a partial result is the thing worth avoiding here. Registrations that then fail are refunded to the balance automatically.\n\n**`mode: \"connect\"`** costs nothing. We create the DNS zone and hand back nameservers; the domain sits in `needs_dns` until it actually points at them, which is expected rather than a failure.","tags":["Domains"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"202":{"description":"At least one domain was queued. Poll each `job_id`.","content":{"application/json":{"example":{"queued":[{"domain":"outbound-atlas.com","domain_id":"dom_91af22","job_id":"job_4h29xk","price_cents":1399}],"rejected":[{"domain":"atlas-mail.co","reason":"Already registered by somebody else."}],"charged_cents":1399,"balance_cents":22751,"poll":"Each queued item carries a job_id. Poll GET /jobs/{id} until it succeeds or fails."}}}},"400":{"description":"`invalid_domains` — None of the strings look like domain names.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`plan_limit` — The batch would exceed the plan’s domain allowance. Carries `limit` and `used`.\n`insufficient_balance` — The domain balance will not cover the batch. Carries `requiredCents`, `balanceCents` and `shortfallCents`. Nothing was bought.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Every name was rejected. The `rejected` array says why for each."},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`registrar_unavailable` — The registrar did not answer, so availability is unknown rather than unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["purchase","connect"],"description":"`purchase` registers new names and charges for them. `connect` adopts names you already own."},"domains":{"type":"array","items":{"type":"string"},"description":"The names. Pasted URLs, `www.` prefixes and trailing slashes are stripped rather than rejected, since every one of them unambiguously names the domain intended.","minItems":1,"maxItems":50},"years":{"type":"integer","description":"Registration length. Purchase mode only. The markup is charged once, not once per year.","minimum":1,"maximum":10}},"required":["mode","domains"],"additionalProperties":false},"example":{"mode":"purchase","domains":["outbound-atlas.com","atlas-mail.co"],"years":1}}}}}},"/domains/search":{"post":{"operationId":"searchDomains","summary":"Check availability and price","description":"Send exact names, or a `seed` to generate variations from a brand. Reserves nothing and charges nothing, but it does hit the registrar on our account, which is why it costs a request against the rate limit rather than being open.\n\n`price_cents` is what a purchase will actually take off the balance — the same function prices both. `registrar_cents` is the wholesale figure before our markup, shown so the markup is not a number that only appears on an invoice.","tags":["Domains"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"The registrar answered. Unavailable names are results, not errors.","content":{"application/json":{"example":{"results":[{"domain":"northworks-mail.com","available":true,"price_cents":1399,"registrar_cents":1099,"currency":"USD","premium":false},{"domain":"northworks.com","available":false,"price_cents":null,"registrar_cents":null,"currency":null,"premium":false}],"markup_cents":300,"years":1,"checked":10}}}},"400":{"description":"`invalid_request` — Neither `domains` nor `seed` was sent.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`registrar_unavailable` — The registrar did not answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"domains":{"type":"array","items":{"type":"string"},"description":"Exact names to check. Send this or `seed`.","maxItems":100},"seed":{"type":"string","description":"A brand to generate variations from, e.g. `northworks`. Send this or `domains`.","minLength":2,"maxLength":40},"tlds":{"type":"array","items":{"type":"string"},"description":"Restrict generated suggestions to these TLDs.","maxItems":8},"limit":{"type":"integer","description":"How many suggestions to generate. Applies to `seed` only.","minimum":1,"maximum":100},"years":{"type":"integer","description":"Price a multi-year registration, so the quote matches a purchase for the same term.","minimum":1,"maximum":10}},"additionalProperties":false},"example":{"seed":"northworks","tlds":["com","co"],"limit":10}}}}}},"/domains/{id}":{"get":{"operationId":"getDomain","summary":"One domain, with its DNS state","description":"The `dns` block is the useful part. `expected` is the record we asked the DNS provider to create; `observed` is what a public resolver actually returns. They are stored separately on purpose, because conflating them produces a dashboard that says \"configured\" while mail quietly fails authentication.\n\n`pending_nameservers` is populated on a connected domain that has not been delegated yet. It is what to show the customer.","tags":["Domains"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"The domain exists on this account.","content":{"application/json":{"example":{"id":"dom_91af22","name":"outbound-atlas.com","status":"active","customer_owned":false,"nameservers_verified":true,"nameservers_checked_at":"2026-07-25T04:00:00.000Z","registered_at":"2026-03-02T11:00:00.000Z","expires_at":"2027-03-02T11:00:00.000Z","auto_renew":true,"reputation_score":94,"mailbox_count":6,"daily_capacity":240,"registrar":"dynadot","dns_provider":"cloudflare","local_part_pattern":"first.last","parked":false,"created_at":"2026-03-02T10:58:11.000Z","dns":{"healthy":true,"records":[{"type":"TXT","name":"outbound-atlas.com","purpose":"spf","expected":"v=spf1 include:spf.emailpal.io -all","observed":"v=spf1 include:spf.emailpal.io -all","status":"verified","verified_at":"2026-07-25T04:00:00.000Z"}]},"pending_nameservers":null}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such domain on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}]},"post":{"operationId":"domainAction","summary":"Verify DNS, rewrite DNS, rotate DKIM, or release","description":"An action name rather than a verb per operation. Releasing is not a delete — the registration outlives it — and re-checking DNS mutates nothing, so a REST-shaped mapping would have described these worse than this does.\n\nAll four are queued rather than run inline. Re-checking DNS means resolving against several public resolvers, which is slow and fails in ways that need retrying; doing it in the request would mean a spinner that sometimes just gives up.","tags":["Domains"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"202":{"description":"The job was queued.","content":{"application/json":{"example":{"job_id":"job_8kd12m","status":"queued","poll":"/api/public/v1/jobs/job_8kd12m"}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such domain on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`domain_in_use` — `release` on a domain that still has mailboxes. Delete those first — releasing would bounce anything already sent from them. Carries `mailbox_count`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["verify_dns","rewrite_dns","rotate_dkim","release"],"description":"`verify_dns` re-checks against public resolvers now. `rewrite_dns` republishes every record we manage. `rotate_dkim` publishes a new key and retires the old one once mail signed with it has landed. `release` stops renewal and takes the domain off the account."}},"required":["action"],"additionalProperties":false},"example":{"action":"verify_dns"}}}}}},"/mailboxes":{"get":{"operationId":"listMailboxes","summary":"List mailboxes","description":"`warming.campaign_ready` is the field to gate a campaign on, and `warming.campaign_blocker` says what is missing when it is false. The warming day alone is not enough: a mailbox at its ceiling with 60% placement is worse to send from than one halfway up the curve at 98%.","tags":["Mailboxes"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"Always.","content":{"application/json":{"example":{"data":[{"id":"mbx_2c81f0","email":"ada.lovelace@outbound-atlas.com","domain_id":"dom_91af22","domain":"outbound-atlas.com","display_name":"Ada Lovelace","first_name":"Ada","last_name":"Lovelace","status":"warming","daily_limit":40,"warming":{"enabled":true,"state":"ramping","profile":"accelerated","day":9,"campaign_ready":false,"campaign_blocker":"Placement has not been measured for long enough yet."},"health_score":86,"inbox_rate":0.94,"smtp":{"host":"smtp.emailpal.io","port":587},"imap":{"host":"imap.emailpal.io","port":993},"created_at":"2026-06-30T08:15:00.000Z"}],"has_more":true,"next_cursor":"2026-06-30T08:15:00.000Z"}}}},"400":{"description":"`invalid_status` — `status` is not one of the listed values.\n`invalid_pagination` — `limit` is out of range or the cursor is unknown.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`read_failed` — The query failed.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"How many to return.","minimum":1,"maximum":200},"description":"How many to return."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","description":"The `next_cursor` from the previous page. Keyset rather than an offset, so rows created while you page do not shift the window and make you miss one.","maxLength":200},"description":"The `next_cursor` from the previous page. Keyset rather than an offset, so rows created while you page do not shift the window and make you miss one."},{"name":"domain_id","in":"query","required":false,"schema":{"type":"string","description":"Only mailboxes on this domain."},"description":"Only mailboxes on this domain."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["provisioning","warming","active","paused","suspended","failed","deleted"],"description":"Only mailboxes in this state."},"description":"Only mailboxes in this state."}]},"post":{"operationId":"createMailboxes","summary":"Create mailboxes","description":"Two ways to say what you want. Send `personas` when the names matter — real-looking first and last names are part of why warming works — or send `count` and let us generate plausible ones against the domain’s local-part pattern.\n\nThe addresses are recorded immediately and provisioned by a job, so the response has the addresses in it before the mail server has them. `overageBillable` is true when the batch takes the account past its included allowance; those mailboxes are billed rather than refused.","tags":["Mailboxes"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"202":{"description":"The addresses were recorded and provisioning was queued.","content":{"application/json":{"example":{"job_id":"job_5m71pq","poll":"/api/public/v1/jobs/job_5m71pq","mailboxes":[{"id":"mbx_2c81f0","email":"ada.lovelace@outbound-atlas.com"},{"id":"mbx_2c81f1","email":"grace.hopper@outbound-atlas.com"}],"overage_billable":false,"note":"Provisioning now. Credentials are available from the export endpoint once the job finishes."}}}},"400":{"description":"`nothing_requested` — Neither `personas` nor `count` was sent.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`plan_limit` — The batch would pass the plan’s hard ceiling. Carries `limit` and `used`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`domain_not_found` — No such domain on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`domain_not_ready` — The domain is not `ready` or `active` yet — usually DNS has not verified. Carries `status`.\n`address_taken` — One of the addresses already exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`insert_failed` — The addresses could not be recorded.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain_id":{"type":"string","description":"The domain to create them on. Must be `ready` or `active`."},"personas":{"type":"array","items":{"type":"object"},"description":"Objects of `{ first_name, last_name }`. `first_name` is required and capped at 40 characters; `last_name` is optional. Send this or `count`.","maxItems":100},"count":{"type":"integer","description":"Generate this many personas instead of naming them. Send this or `personas`.","minimum":1,"maximum":100},"pattern":{"type":"string","enum":["first","first.last","firstlast","f.last","first_last","firstl"],"description":"How to build the local part from a name."},"daily_limit":{"type":"integer","description":"Messages a day once warmed. Above 50 is permitted and unwise: reply rates roughly quarter above a hundred a day.","minimum":1,"maximum":100},"warming":{"type":"boolean","description":"Start warming as soon as the mailbox exists."},"warming_profile":{"type":"string","enum":["standard","accelerated","immediate"],"description":"`immediate` removes the ramp entirely and is only safe on a domain with real sending history behind it."}},"required":["domain_id"],"additionalProperties":false},"example":{"domain_id":"dom_91af22","count":6,"pattern":"first.last","daily_limit":40,"warming_profile":"accelerated"}}}}}},"/mailboxes/{id}":{"get":{"operationId":"getMailbox","summary":"One mailbox, with health","description":"Everything the list returns plus the signature, placement rates, sync timestamps and — when provisioning failed — the reason. `failure_reason` is returned rather than hidden because a `status: \"failed\"` with no explanation is the single most common thing a customer opens a ticket about.\n\nDoes not include the password. Credentials come from the export endpoint, which needs `admin` and is audited.","tags":["Mailboxes"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"The mailbox exists on this account.","content":{"application/json":{"example":{"id":"mbx_2c81f0","email":"ada.lovelace@outbound-atlas.com","domain_id":"dom_91af22","domain":"outbound-atlas.com","display_name":"Ada Lovelace","signature":null,"status":"warming","daily_limit":40,"warming":{"enabled":true,"state":"ramping","profile":"accelerated","day":9,"started_at":"2026-07-16T06:00:00.000Z","campaign_ready":false,"campaign_ready_at":null,"campaign_blocker":"Placement has not been measured for long enough yet."},"health_score":86,"inbox_rate":0.94,"spam_rate":0.02,"smtp":{"host":"smtp.emailpal.io","port":587},"imap":{"host":"imap.emailpal.io","port":993},"last_send_at":"2026-07-25T14:20:00.000Z","last_sync_at":"2026-07-25T14:51:00.000Z","provisioned_at":"2026-06-30T08:17:41.000Z","failure_reason":null,"created_at":"2026-06-30T08:15:00.000Z"}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such mailbox on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}]},"patch":{"operationId":"updateMailbox","summary":"Change limits, warming, or pause it","description":"Fields rather than an action name, because these are settings and a client that wants to pause a mailbox and drop its limit should not need two requests that can half-fail.\n\n`warming: false` and `warming_paused: true` are different things and the difference is money. Disabling stops the warming charge. Pausing stops the sending, keeps the mailbox’s place on the ramp, and keeps billing. Sending both is rejected rather than guessed at.","tags":["Mailboxes"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"200":{"description":"Settings were applied. Nothing needed the mail server.","content":{"application/json":{"example":{"id":"mbx_2c81f0","applied":["daily_limit","warming_paused"],"job_id":null}}}},"202":{"description":"`paused` was sent, so a job was queued. Poll it."},"400":{"description":"`invalid_request` — No fields were sent, or both `warming: false` and `warming_paused: true` were.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such mailbox on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`mailbox_suspended` — The mailbox is suspended. That is an enforcement action and cannot be lifted through the API.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`update_failed` — The change could not be applied.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"daily_limit":{"type":"integer","description":"Messages a day once warmed.","minimum":1,"maximum":100},"warming":{"type":"boolean","description":"Whether warming runs at all. `false` also stops the monthly warming charge for this mailbox."},"warming_paused":{"type":"boolean","description":"Pause or resume sending while leaving warming enabled. Still billed — the mailbox keeps its ramp progress."},"warming_profile":{"type":"string","enum":["standard","accelerated","immediate"],"description":"Changing this keeps the days already done and moves the mailbox to that point on the new curve, so it neither restarts nor skips ahead of its own history."},"paused":{"type":"boolean","description":"Pause the mailbox itself. Reaches the mail server, so this queues a job."},"display_name":{"type":"string","description":"The From name.","minLength":1,"maxLength":80},"signature":{"type":"string","description":"Appended to outbound mail.","maxLength":2000}},"additionalProperties":false},"example":{"daily_limit":45,"warming_paused":true}}}}},"delete":{"operationId":"deleteMailbox","summary":"Delete a mailbox","description":"Irreversible, and it discards the warming history — a replacement address starts from zero and has to earn its placement evidence again. Pausing is almost always the better answer if the goal is only to stop sending.","tags":["Mailboxes"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"202":{"description":"Deletion was queued.","content":{"application/json":{"example":{"job_id":"job_9xq04b","status":"queued","poll":"/api/public/v1/jobs/job_9xq04b"}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such mailbox on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}]}},"/mailboxes/export":{"get":{"operationId":"exportMailboxes","summary":"Credentials as CSV","description":"Returns `text/csv`, not JSON. **Contains plaintext passwords**, which is why it needs `admin` on its own rather than inheriting from `write`, and why every call is written to the audit log.\n\nOnly mailboxes that have finished provisioning and are `warming`, `active` or `paused` are included. The count is returned in the `X-EmailPal-Exported` header, and anything skipped for unreadable credentials in `X-EmailPal-Skipped`.","tags":["Mailboxes"],"security":[{"bearerAuth":[]}],"x-required-scope":"admin","responses":{"200":{"description":"At least one mailbox had readable credentials.","content":{"text/csv":{"example":"email,display_name,first_name,last_name,smtp_username,smtp_password,smtp_host,smtp_port,smtp_security,imap_username,imap_password,imap_host,imap_port,imap_security,daily_limit\nada.lovelace@outbound-atlas.com,Ada Lovelace,Ada,Lovelace,ada.lovelace@outbound-atlas.com,x7Kp...,smtp.emailpal.io,587,STARTTLS,ada.lovelace@outbound-atlas.com,x7Kp...,imap.emailpal.io,993,SSL,40"}}},"400":{"description":"`invalid_format` — `format` is not one of the three.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`nothing_to_export` — No mailbox matched, or none had credentials yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`read_failed` — The query failed.\n`credentials_unreadable` — Every matching row failed to decrypt.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["generic","smartlead","instantly"],"description":"Column layout. `smartlead` and `instantly` match those tools’ importers exactly; `generic` carries everything including security modes."},"description":"Column layout. `smartlead` and `instantly` match those tools’ importers exactly; `generic` carries everything including security modes."},{"name":"domain_id","in":"query","required":false,"schema":{"type":"string","description":"Only mailboxes on this domain."},"description":"Only mailboxes on this domain."},{"name":"mailbox_ids","in":"query","required":false,"schema":{"type":"string","description":"A comma-separated list of specific mailbox ids."},"description":"A comma-separated list of specific mailbox ids."}]}},"/warming":{"get":{"operationId":"getWarming","summary":"Progress, placement and cost","description":"The question this answers is whether to start a campaign. `campaign_ready` is our answer to the combined question and `campaign_blocker` says what is missing when it is false, so a client does not have to reimplement the rule from the parts.\n\nBehind it: `inbox_rate` below roughly 0.9 means the domain is not trusted yet whatever day the ramp says it is on, `ramp_progress` is how far today’s volume has climbed toward the ceiling rather than how much time has passed, and `rescued_from_spam` above zero is an early warning even while the inbox rate still looks acceptable.\n\nRead from a daily rollup rather than counting messages, so polling this is cheap.","tags":["Warming"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"Always. An account with no warming mailboxes gets zeroes and a note.","content":{"application/json":{"example":{"summary":{"mailboxes":96,"ramped":41,"billable_mailboxes":96,"estimated_monthly_cost_cents":5760,"campaign_ready":38,"average_health":88,"average_inbox_rate":0.9412},"mailboxes":[{"mailbox_id":"mbx_2c81f0","email":"ada.lovelace@outbound-atlas.com","status":"warming","state":"ramping","warming_profile":"accelerated","day":9,"started_at":"2026-07-16T06:00:00.000Z","ramp_progress":0.55,"campaign_ready":false,"campaign_ready_at":null,"campaign_blocker":"Placement has not been measured for long enough yet.","health_score":86,"inbox_rate":0.94,"spam_rate":0.02,"daily_limit":40,"window":{"days":7,"sent":132,"received":128,"replied":71,"landed_in_inbox":121,"landed_in_spam":7,"rescued_from_spam":7},"daily":[{"day":"2026-07-25","target":22,"sent":22,"inbox":21,"spam":1}]}]}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`read_failed` — The query failed.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"mailbox_id","in":"query","required":false,"schema":{"type":"string","description":"Just one mailbox."},"description":"Just one mailbox."},{"name":"days","in":"query","required":false,"schema":{"type":"integer","description":"Length of the measurement window.","minimum":1,"maximum":30},"description":"Length of the measurement window."}]},"post":{"operationId":"setWarming","summary":"Enable, disable, pause or resume in bulk","description":"Up to five hundred mailboxes in one call. Here rather than as a loop over `PATCH /mailboxes/{id}` because switching warming on for a hundred new addresses is one decision, and a client making a hundred calls to express it will hit the rate limit doing so and then have a hundred partial outcomes to reconcile.\n\nFour verbs, not a boolean, because **`disable` stops the charge and `pause` does not**. A paused mailbox keeps its place on the ramp and stays billed; the response repeats this in `billing.note` so a confirmation dialog can quote it.\n\nMailboxes that could not be changed come back in `skipped` with a reason each, rather than failing the whole call.","tags":["Warming"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"200":{"description":"At least one mailbox changed.","content":{"application/json":{"example":{"action":"disable","changed":[{"mailbox_id":"mbx_2c81f0","email":"ada.lovelace@outbound-atlas.com"}],"skipped":[{"mailbox_id":"mbx_2c81f1","email":"grace.hopper@outbound-atlas.com","reason":"This mailbox is suspended, which is an enforcement action. Get in touch."}],"billing":{"warming_mailboxes":95,"estimated_monthly_cost_cents":5700,"note":"Only mailboxes with warming enabled are billed, and leased pre-warmed inboxes never are."}}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Every mailbox was skipped. The body still lists why for each."},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`read_failed` — The mailboxes could not be read.\n`update_failed` — The change could not be applied.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["enable","disable","pause","resume"],"description":"`disable` stops the monthly charge. `pause` does not."},"mailbox_ids":{"type":"array","items":{"type":"string"},"description":"The mailboxes to change. Ones that are not on this account are skipped, not errors.","minItems":1,"maxItems":500}},"required":["action","mailbox_ids"],"additionalProperties":false},"example":{"action":"disable","mailbox_ids":["mbx_2c81f0","mbx_2c81f1"]}}}}}},"/inbox":{"get":{"operationId":"listInbox","summary":"List received mail","description":"Newest first, across every mailbox, with a preview. Warming traffic is excluded unless you ask for it: it is the majority of what arrives and none of it is from a real prospect.\n\nBodies are not included. Fetching one means talking to the mail server, so it is a separate call.","tags":["Inbox"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"Always.","content":{"application/json":{"example":{"data":[{"id":"msg_6b20de","mailbox_id":"mbx_2c81f0","mailbox":"ada.lovelace@outbound-atlas.com","folder":"INBOX","message_id":"<CAF=8xQ@mail.example.com>","thread_key":"thr_44ac91","from":{"address":"priya@acme.example","name":"Priya Raman"},"to":["ada.lovelace@outbound-atlas.com"],"subject":"Re: quick question about your rollout","preview":"Thanks for following up — happy to take a look next week.","received_at":"2026-07-25T13:41:02.000Z","seen":false,"flagged":false,"warming":false,"has_attachments":false}],"has_more":false,"next_cursor":null}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`read_failed` — The query failed.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"How many to return.","minimum":1,"maximum":200},"description":"How many to return."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","description":"The `next_cursor` from the previous page. Keyset rather than an offset, so rows created while you page do not shift the window and make you miss one.","maxLength":200},"description":"The `next_cursor` from the previous page. Keyset rather than an offset, so rows created while you page do not shift the window and make you miss one."},{"name":"mailbox_id","in":"query","required":false,"schema":{"type":"string","description":"Only mail for this mailbox."},"description":"Only mail for this mailbox."},{"name":"unread","in":"query","required":false,"schema":{"type":"boolean","description":"Only messages that have not been marked seen."},"description":"Only messages that have not been marked seen."},{"name":"include_warming","in":"query","required":false,"schema":{"type":"boolean","description":"Include warming traffic. Rarely what you want."},"description":"Include warming traffic. Rarely what you want."},{"name":"q","in":"query","required":false,"schema":{"type":"string","description":"Case-insensitive match against sender, subject and preview."},"description":"Case-insensitive match against sender, subject and preview."}]}},"/inbox/{id}":{"get":{"operationId":"getMessage","summary":"One message, with its body","description":"Returns **202 with `status: \"fetching\"`** when the body is not cached, because getting it means an IMAP round trip we will not hold a connection open for. Poll this same URL — not `/jobs/{id}` — until `status` is `ready`. It is usually a second or two.\n\nThe headers and preview are present in both cases, so a client can render the message frame immediately and fill the body in when it arrives.","tags":["Inbox"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"`status` is `ready` and `body` is populated.","content":{"application/json":{"example":{"status":"ready","id":"msg_6b20de","mailbox_id":"mbx_2c81f0","folder":"INBOX","message_id":"<CAF=8xQ@mail.example.com>","thread_key":"thr_44ac91","in_reply_to":"<CAF=7wP@mail.outbound-atlas.com>","from":{"address":"priya@acme.example","name":"Priya Raman"},"to":["ada.lovelace@outbound-atlas.com"],"subject":"Re: quick question about your rollout","sent_at":"2026-07-25T13:40:55.000Z","received_at":"2026-07-25T13:41:02.000Z","seen":false,"flagged":false,"warming":false,"has_attachments":false,"body":{"text":"Thanks for following up — happy to take a look next week.","html":"<p>Thanks for following up — happy to take a look next week.</p>","fetched_at":"2026-07-25T13:41:09.000Z"}}}}},"202":{"description":"`status` is `fetching` and `body` is null. Poll this URL again."},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such message on this account, or it was deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}]},"patch":{"operationId":"updateMessage","summary":"Mark read or flagged","description":"Send at least one of the two. The change is written locally and pushed to the mail server on the next sync.","tags":["Inbox"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"200":{"description":"Applied.","content":{"application/json":{"example":{"id":"msg_6b20de","seen":true}}}},"400":{"description":"`invalid_request` — Neither `seen` nor `flagged` was sent.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such message on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"seen":{"type":"boolean","description":"Whether the message is marked read."},"flagged":{"type":"boolean","description":"Whether the message is flagged."}},"additionalProperties":false},"example":{"seen":true}}}}},"delete":{"operationId":"deleteMessage","summary":"Delete a message","description":"Queued, because it has to reach the mail server. The message disappears from listings immediately.","tags":["Inbox"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"202":{"description":"Deletion was queued.","content":{"application/json":{"example":{"id":"msg_6b20de","job_id":"job_1t88za"}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such message on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}]}},"/inbox/send":{"post":{"operationId":"sendMessage","summary":"Send or reply from a mailbox","description":"Sends as the named mailbox. Pass `reply_to_id` to thread the message properly — we derive the `In-Reply-To` and `References` headers from the original, which is what stops a reply arriving as a new conversation.\n\nRecipients are checked against the suppression list before the message is queued. A suppressed address is a 409 rather than a silent drop, because a client that thinks it sent something it did not is worse off than one that got an error.","tags":["Inbox"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"202":{"description":"The message was recorded and queued for sending.","content":{"application/json":{"example":{"id":"out_3v90kc","from":"ada.lovelace@outbound-atlas.com","job_id":"job_2p61hd","poll":"/api/public/v1/jobs/job_2p61hd"}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such mailbox on this account.\n`reply_target_not_found` — `reply_to_id` is not a message on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`mailbox_suspended` — The mailbox is suspended.\n`mailbox_provisioning` — The mailbox has no credentials yet.\n`recipient_suppressed` — A recipient is on the suppression list. Carries `address`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`insert_failed` — The message could not be recorded.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mailbox_id":{"type":"string","description":"Which mailbox sends it."},"to":{"type":"array","items":{"type":"string"},"description":"Recipients.","minItems":1,"maxItems":10},"cc":{"type":"array","items":{"type":"string"},"description":"Carbon copies.","maxItems":10},"subject":{"type":"string","description":"Subject line.","minLength":1,"maxLength":500},"body":{"type":"string","description":"Plain text body.","minLength":1,"maxLength":50000},"reply_to_id":{"type":"string","description":"The id of the message being replied to. Sets the threading headers."}},"required":["mailbox_id","to","subject","body"],"additionalProperties":false},"example":{"mailbox_id":"mbx_2c81f0","to":["priya@acme.example"],"subject":"Re: quick question about your rollout","body":"Next week works. Would Tuesday at 10 suit?","reply_to_id":"msg_6b20de"}}}}}},"/inbox/refresh":{"post":{"operationId":"refreshInbox","summary":"Force a sync now","description":"Mail syncs on its own schedule; this is for when you need it sooner. Rate limited to one sync per mailbox per thirty seconds, so calling it in a loop is harmless and also pointless — the extra calls come back with `queued: 0`.\n\nOmit `mailbox_id` to sync every eligible mailbox on the account, up to two hundred.","tags":["Inbox"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"202":{"description":"Sync jobs were queued, or were already running.","content":{"application/json":{"example":{"queued":1,"mailboxes":1,"note":"Syncing now. New mail usually appears within a few seconds."}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`nothing_to_sync` — No mailbox was eligible — they must be provisioned and `active`, `warming` or `paused`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"mailbox_id":{"type":"string","description":"Sync one mailbox instead of all of them."}},"additionalProperties":false},"example":{"mailbox_id":"mbx_2c81f0"}}}}}},"/jobs":{"get":{"operationId":"listJobs","summary":"Recent jobs","description":"Newest first. Filter by `type` to watch one kind of work, or by `status` to find what failed.","tags":["Jobs"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"Always.","content":{"application/json":{"example":{"data":[{"id":"job_4h29xk","type":"domain.purchase","status":"succeeded","terminal":true,"attempts":1,"max_attempts":2,"progress":100,"progress_message":null,"error":null,"retry_after":null,"started_at":"2026-07-25T14:02:11.000Z","finished_at":"2026-07-25T14:02:29.000Z","created_at":"2026-07-25T14:02:09.000Z"}],"has_more":false,"next_cursor":null}}}},"400":{"description":"`invalid_status` — `status` is not one of the listed values.\n`invalid_pagination` — `limit` is out of range or the cursor is unknown.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`read_failed` — The query failed.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"How many to return.","minimum":1,"maximum":200},"description":"How many to return."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","description":"The `next_cursor` from the previous page. Keyset rather than an offset, so rows created while you page do not shift the window and make you miss one.","maxLength":200},"description":"The `next_cursor` from the previous page. Keyset rather than an offset, so rows created while you page do not shift the window and make you miss one."},{"name":"type","in":"query","required":false,"schema":{"type":"string","description":"Exact job type, e.g. `domain.purchase`."},"description":"Exact job type, e.g. `domain.purchase`."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["queued","running","succeeded","failed","cancelled","dead"],"description":"Only jobs in this state."},"description":"Only jobs in this state."}]}},"/jobs/{id}":{"get":{"operationId":"getJob","summary":"One job","description":"The poll target for everything asynchronous. `terminal` is the field to loop on — it is true for `succeeded`, `failed`, `dead` and `cancelled`, so a client does not have to keep its own list of which statuses mean stop.\n\n`retry_after` is set when a job failed but has attempts left, and is the time the queue will try again on its own. A `dead` job has run out of attempts and will not.","tags":["Jobs"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"The job exists on this account.","content":{"application/json":{"example":{"id":"job_4h29xk","type":"domain.purchase","status":"running","terminal":false,"attempts":1,"max_attempts":2,"progress":50,"progress_message":"Registering with the registrar","error":null,"retry_after":null,"started_at":"2026-07-25T14:02:11.000Z","finished_at":null,"created_at":"2026-07-25T14:02:09.000Z"}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such job on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}]},"post":{"operationId":"jobAction","summary":"Retry or cancel a job","description":"Retry puts a failed, dead or cancelled job back on the queue with its attempt count reset. Cancel stops one that has not finished.\n\nRetrying is safe: every job that touches money or a registrar carries an idempotency key, so a retried purchase cannot buy the same domain twice.","tags":["Jobs"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"200":{"description":"Cancelled.","content":{"application/json":{"example":{"id":"job_4h29xk","status":"queued"}}}},"202":{"description":"Requeued. Poll it again."},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such job on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`job_finished` — Cancel on a job that already succeeded or was cancelled.\n`not_retryable` — Retry on a job that is not `failed`, `dead` or `cancelled`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["retry","cancel"],"description":"What to do with it."}},"required":["action"],"additionalProperties":false},"example":{"action":"retry"}}}}}},"/suppressions":{"get":{"operationId":"listSuppressions","summary":"List suppressions, or check one address","description":"Two modes. Pass `address` to ask about one address and get a direct yes or no — that is the call to make before a send, and it answers for domain-level suppressions too, which a naive lookup against your own copy of the list would miss.\n\nOmit it to page through everything suppressed on the account.","tags":["Compliance"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"Always.","content":{"application/json":{"example":{"address":"priya@acme.example","suppressed":true,"reason":"Recipient unsubscribed.","source":"unsubscribe","scope":"address","suppressed_at":"2026-07-11T08:02:00.000Z"}}}},"400":{"description":"`invalid_address` — `address` is not a valid email address.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`read_failed` — The query failed.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"How many to return.","minimum":1,"maximum":200},"description":"How many to return."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string","description":"The `next_cursor` from the previous page. Keyset rather than an offset, so rows created while you page do not shift the window and make you miss one.","maxLength":200},"description":"The `next_cursor` from the previous page. Keyset rather than an offset, so rows created while you page do not shift the window and make you miss one."},{"name":"address","in":"query","required":false,"schema":{"type":"string","description":"Check this one address. Pagination is ignored when it is present."},"description":"Check this one address. Pagination is ignored when it is present."}]},"post":{"operationId":"createSuppression","summary":"Suppress an address or a domain","description":"Exactly one of `address` or `domain`. Suppressing a domain stops mail to every address on it, which is the right response to an abuse complaint from a company rather than a person.\n\nSuppressing something already suppressed returns 200 rather than an error, so an integration that replays its own queue does not have to special-case it.","tags":["Compliance"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"200":{"description":"Already suppressed. The body carries `already_suppressed: true`."},"201":{"description":"Newly suppressed.","content":{"application/json":{"example":{"id":"sup_5d10ba","address":"priya@acme.example","domain":null,"created_at":"2026-07-25T15:10:00.000Z","note":"Suppressed across every mailbox on this account."}}}},"400":{"description":"`invalid_request` — Neither or both of `address` and `domain` were sent.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`insert_failed` — The suppression could not be recorded.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"address":{"type":"string","description":"The address to suppress. Send this or `domain`.","maxLength":320},"domain":{"type":"string","description":"The domain to suppress. Send this or `address`.","minLength":3,"maxLength":253},"reason":{"type":"string","description":"Free text, kept for the audit trail.","minLength":3,"maxLength":200}},"additionalProperties":false},"example":{"address":"priya@acme.example","reason":"Asked to be removed by reply."}}}}}},"/lists/verify":{"post":{"operationId":"verifyList","summary":"Check a recipient list before sending to it","description":"Send the raw file contents — CSV, one-per-line, or anything with addresses in it — and we extract and classify them. A list that would mostly bounce is worth knowing about before it costs you a domain rather than after.\n\nAsynchronous, and the poll target is `GET /lists/{id}` rather than the job. The verdict lives on the upload.","tags":["Compliance"],"security":[{"bearerAuth":[]}],"x-required-scope":"write","responses":{"202":{"description":"Addresses were extracted and verification was queued.","content":{"application/json":{"example":{"upload_id":"lst_7c31ee","addresses":2,"job_id":"job_6j40wl","poll":"/api/public/v1/lists/lst_7c31ee"}}}},"400":{"description":"`no_addresses` — Nothing in the content parsed as an email address.\n`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`insert_failed` — The upload could not be recorded.\n`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string","description":"The raw file contents.","minLength":3,"maxLength":12000000},"filename":{"type":"string","description":"Shown in the dashboard so a human can tell uploads apart.","maxLength":200}},"required":["content"],"additionalProperties":false},"example":{"filename":"q3-prospects.csv","content":"email\npriya@acme.example\nsam@example.org"}}}}}},"/lists/{id}":{"get":{"operationId":"getList","summary":"The verdict on a submitted list","description":"Poll until `status` leaves `pending` and `verifying`. `sendable` is the field to branch on: it is true for `passed` and `flagged`, and false for `blocked` and `failed`.\n\n`flagged` means send it but know what is in it. `blocked` means the list would damage the reputation of every domain on the account, and `blocked_reason` says which measure crossed the line.","tags":["Compliance"],"security":[{"bearerAuth":[]}],"x-required-scope":"read","responses":{"200":{"description":"The upload exists on this account, including while it is still verifying.","content":{"application/json":{"example":{"id":"lst_7c31ee","filename":"q3-prospects.csv","status":"flagged","sendable":true,"counts":{"total":2140,"valid":1802,"invalid":214,"risky":88,"suppressed":12,"duplicate":24},"blocked_reason":null,"verified_at":"2026-07-25T15:22:41.000Z","created_at":"2026-07-25T15:21:03.000Z"}}}},"400":{"description":"`invalid_request` — The body failed validation. The message names the fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`missing_credentials` — No `Authorization: Bearer` header.\n`invalid_credentials` — The key is not one of ours.\n`key_revoked` — The key was revoked. Create a new one.\n`key_expired` — The key passed its expiry date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`insufficient_scope` — The key lacks the scope this endpoint needs. Carries `granted` and `required`.\n`account_suspended` — The account is suspended or closed. The credentials are fine, which is worth knowing before you rotate a key that was never the problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — No such upload on this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — The hour’s allowance is spent. Carries `limit`, `used` and `resetAt`, plus a `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error` — Something broke on our side. Nothing was changed — safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource id."}]}}}}