{"components":{"parameters":{"Since":{"description":"Only items (messages, SMS, or captured requests) received strictly after this RFC 3339 timestamp. Feed back the last summary's `received_at` as a cursor — the row carrying that exact timestamp is not repeated.","in":"query","name":"since","required":false,"schema":{"format":"date-time","type":"string"}},"Wait":{"description":"Long-poll window in seconds; default 0 answers immediately. Clamped to 60 — larger values are accepted and treated as 60. The request returns as soon as a matching item arrives; an empty window still answers 200 with an empty list.","in":"query","name":"wait","required":false,"schema":{"minimum":0,"type":"integer"}}},"responses":{"RateLimited":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Per-key rate limit exceeded. `Retry-After` says when the next request will be admitted.","headers":{"Retry-After":{"description":"Seconds until the bucket refills.","schema":{"type":"integer"}}}},"ServerError":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Unexpected server error. `detail` is null — internals are never leaked."},"Unauthorized":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Missing bearer token, malformed key, or unknown/revoked/invalid API key."}},"schemas":{"ApiKey":{"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"label":{"type":["string","null"]},"prefix":{"type":"string"},"revoked_at":{"description":"Null while the key is active.","format":"date-time","type":["string","null"]}},"required":["id","prefix","label","created_at","revoked_at"],"type":"object"},"ApiKeyCreated":{"properties":{"id":{"format":"uuid","type":"string"},"key":{"description":"The full bearer token (`mfx_…`) — shown exactly once.","type":"string"},"prefix":{"description":"8-char lookup prefix.","type":"string"}},"required":["id","prefix","key"],"type":"object"},"ApiKeyList":{"properties":{"keys":{"items":{"$ref":"#/components/schemas/ApiKey"},"type":"array"}},"required":["keys"],"type":"object"},"AttachmentMeta":{"properties":{"content_type":{"description":"Declared MIME type; defaults to application/octet-stream.","type":"string"},"filename":{"type":["string","null"]},"inline":{"description":"True for cid-referenced inline parts (e.g. embedded images).","type":"boolean"},"size":{"description":"Decoded byte length.","type":"integer"}},"required":["filename","content_type","size","inline"],"type":"object"},"AuthResult":{"description":"RFC 8601 result vocabulary. DNS trouble degrades to `temperror`.","enum":["pass","fail","softfail","neutral","none","temperror","permerror"],"type":"string"},"CreateDomainRequest":{"properties":{"fqdn":{"description":"Fully-qualified domain like mail.example.com; trimmed, trailing dot stripped, lowercased. Prefer a dedicated subdomain.","maxLength":253,"type":"string"}},"required":["fqdn"],"type":"object"},"CreateHookRequest":{"description":"All fields optional; an empty or absent body creates an unlabeled hook with no expiry.","properties":{"label":{"description":"Human label shown in lists and the dashboard.","maxLength":200,"type":"string"},"ttl_seconds":{"description":"Auto-delete the hook (and its captured requests) after this many seconds (max 30 days). Omit for no expiry.","maximum":2592000,"minimum":60,"type":"integer"}},"type":"object"},"CreateInboxRequest":{"description":"All fields optional; an empty or absent body creates a random address on the shared domain with no expiry.","properties":{"domain":{"description":"A shared domain or one of the account's verified custom domains. Omit for the shared domain.","type":"string"},"local_part":{"description":"Lowercased on input; must not start or end with a dot. Omit for a random 10-char local part.","maxLength":64,"minLength":1,"pattern":"^[a-z0-9._-]+$","type":"string"},"ttl_seconds":{"description":"Auto-expire the inbox after this many seconds (max 30 days). Expired inboxes 550-reject new mail and are swept hourly. Omit for no expiry.","maximum":2592000,"minimum":60,"type":"integer"}},"type":"object"},"CreateKeyRequest":{"properties":{"label":{"description":"Display label; blank is treated as no label.","type":"string"}},"type":"object"},"CreateWebhookRequest":{"properties":{"description":{"description":"Optional; blank is treated as none.","type":"string"},"url":{"description":"Must be https and resolve to a public IP.","format":"uri","type":"string"}},"required":["url"],"type":"object"},"DkimAuth":{"properties":{"domain":{"type":"string"},"error":{"description":"Present only for non-pass results — e.g. a body-hash mismatch reports `neutral` with the reason here (RFC 6376 §6.1).","type":"string"},"result":{"$ref":"#/components/schemas/AuthResult"},"selector":{"type":"string"}},"required":["result","domain","selector"],"type":"object"},"DmarcAuth":{"properties":{"domain":{"description":"RFC5322.From domain; present only when non-empty.","type":"string"},"policy":{"description":"Present only when a DMARC record exists.","enum":["none","quarantine","reject"],"type":"string"},"result":{"$ref":"#/components/schemas/AuthResult"}},"required":["result"],"type":"object"},"Domain":{"properties":{"created_at":{"format":"date-time","type":"string"},"fqdn":{"type":"string"},"id":{"format":"uuid","type":"string"},"kind":{"enum":["shared","custom"],"type":"string"},"mx_ok_at":{"format":"date-time","type":["string","null"]},"setup":{"$ref":"#/components/schemas/DomainSetup","description":"DNS records to publish; present on creation and on unverified custom domains in lists."},"verified_at":{"format":"date-time","type":["string","null"]},"verify_txt_token":{"description":"TXT challenge value; null for shared domains.","type":["string","null"]}},"required":["id","fqdn","kind","verify_txt_token","verified_at","mx_ok_at","created_at"],"type":"object"},"DomainList":{"properties":{"domains":{"items":{"$ref":"#/components/schemas/Domain"},"type":"array"}},"required":["domains"],"type":"object"},"DomainSetup":{"properties":{"mx":{"properties":{"name":{"type":"string"},"priority":{"type":"integer"},"value":{"description":"The MX hostname to point at.","type":"string"}},"required":["name","value","priority"],"type":"object"},"txt":{"properties":{"name":{"description":"Record name, `_mfx.<fqdn>`.","type":"string"},"value":{"description":"Record value, `mfx-verify=<token>`.","type":"string"}},"required":["name","value"],"type":"object"}},"required":["txt","mx"],"type":"object"},"DomainVerifyResult":{"properties":{"fqdn":{"type":"string"},"id":{"format":"uuid","type":"string"},"mx_ok":{"description":"This check run saw the MX record.","type":"boolean"},"mx_ok_at":{"format":"date-time","type":["string","null"]},"setup":{"$ref":"#/components/schemas/DomainSetup"},"txt_ok":{"description":"This check run saw the TXT challenge.","type":"boolean"},"verified_at":{"format":"date-time","type":["string","null"]}},"required":["id","fqdn","txt_ok","mx_ok","verified_at","mx_ok_at","setup"],"type":"object"},"Extracted":{"description":"Extraction results computed at receipt. `text`, `auth`, and `spam` are conditionally present keys — see their descriptions.","properties":{"auth":{"$ref":"#/components/schemas/ExtractedAuth"},"links":{"items":{"$ref":"#/components/schemas/ExtractedLink"},"type":"array"},"otp":{"$ref":"#/components/schemas/ExtractedOtp"},"spam":{"$ref":"#/components/schemas/ExtractedSpam"},"text":{"description":"Plain-text rendering of the HTML part; present only when the message had no text/plain part.","type":"string"}},"required":["otp","links"],"type":"object"},"ExtractedAuth":{"description":"Receive-side SPF/DKIM/DMARC verdicts (RFC 8601 vocabulary). The whole key is absent when verification is disabled on the deployment — absence means \"not verified\", never \"failed\". Observation only: verdicts never affect acceptance.","properties":{"dkim":{"description":"One entry per DKIM signature; unsigned mail is an empty array.","items":{"$ref":"#/components/schemas/DkimAuth"},"type":"array"},"dmarc":{"$ref":"#/components/schemas/DmarcAuth"},"spf":{"$ref":"#/components/schemas/SpfAuth"}},"required":["spf","dkim","dmarc"],"type":"object"},"ExtractedLink":{"properties":{"class":{"description":"Class guess from URL path + anchor keywords; magic-link/login URLs class as `verify`.","enum":["verify","reset","unsubscribe","other"],"type":"string"},"text":{"description":"Anchor text; null for bare-text URLs.","type":["string","null"]},"url":{"type":"string"}},"required":["url","text","class"],"type":"object"},"ExtractedOtp":{"properties":{"best":{"description":"Top-ranked code, or null when no candidate scores above the confidence threshold.","type":["string","null"]},"candidates":{"description":"At most 5, sorted by descending score.","items":{"$ref":"#/components/schemas/OtpCandidate"},"type":"array"}},"required":["best","candidates"],"type":"object"},"ExtractedSpam":{"description":"SpamAssassin content analysis (local rules only — no network/RBL tests, so scores are deterministic). The whole key is absent when scoring is disabled, the daemon was unreachable, or the message exceeded the scoring size cap — absence means \"not scored\", never \"failed\". Observation only: the verdict never affects acceptance.","properties":{"rules":{"description":"The rules that fired, with their individual score contributions; empty when nothing matched.","items":{"$ref":"#/components/schemas/SpamRule"},"type":"array"},"score":{"description":"Total SpamAssassin score; higher is spammier. May be negative.","type":"number"},"threshold":{"description":"The score at or above which the message is classed spam (default 5.0).","type":"number"},"verdict":{"description":"SpamAssassin's own decision (its `Spam: True/False`), not a client-side threshold recomputation.","enum":["spam","ham"],"type":"string"}},"required":["score","threshold","verdict","rules"],"type":"object"},"FollowOutcome":{"properties":{"final_url":{"description":"URL of the final, non-redirect response.","type":"string"},"ok":{"description":"True when `status` is 2xx.","type":"boolean"},"redirects":{"description":"Redirect hops followed (max 5).","type":"integer"},"requested_url":{"type":"string"},"status":{"description":"HTTP status of the final response — reported, not judged.","type":"integer"}},"required":["requested_url","final_url","status","redirects","ok"],"type":"object"},"FollowRequest":{"description":"Selects which extracted link to follow. Pass at most one field; with none, the first `verify`-classed link is followed.","properties":{"class":{"description":"First link of this class.","enum":["verify","reset","unsubscribe","other"],"type":"string"},"index":{"description":"Zero-based index into the extracted links.","minimum":0,"type":"integer"},"url":{"description":"Exact match against a URL in the extracted links.","type":"string"}},"type":"object"},"Hook":{"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"ingest_url":{"description":"The public URL to configure in the app under test. Any method; bodies up to 256KB. Successful captures always answer the fixed `200 {\"ok\":true}` — responses are not configurable; over-size bodies answer 413, exhausted quotas 429 with Retry-After, and an unknown token 404.","type":"string"},"label":{"type":["string","null"]},"token":{"description":"The write capability embedded in the ingest URL. Anyone who knows it can add requests to the bin — treat like a webhook secret. It never grants reads.","type":"string"},"ttl_expires_at":{"format":"date-time","type":["string","null"]}},"required":["id","token","ingest_url","label","ttl_expires_at","created_at"],"type":"object"},"HookHeader":{"properties":{"name":{"description":"Lowercased header name.","type":"string"},"value":{"description":"Header value, lossy-decoded when not valid UTF-8.","type":"string"}},"required":["name","value"],"type":"object"},"HookList":{"properties":{"hooks":{"items":{"$ref":"#/components/schemas/Hook"},"type":"array"}},"required":["hooks"],"type":"object"},"HookRequestDetail":{"properties":{"body":{"description":"The body as text. When the raw bytes were not valid UTF-8 text this is a lossy rendering — use `body_base64` for the exact bytes (signature verification needs them).","type":"string"},"body_base64":{"description":"Base64 of the exact body bytes. Present ONLY when `body_is_utf8` is false.","type":"string"},"body_is_utf8":{"type":"boolean"},"body_size":{"type":"integer"},"content_type":{"type":["string","null"]},"expires_at":{"description":"Plan retention, same clock as messages.","format":"date-time","type":"string"},"headers":{"description":"Lowercased names; duplicate values for one name keep their received order, but the relative order of different names is not preserved. The proxy's own x-forwarded-* hop headers are stripped (the sender address is `remote_ip`).","items":{"$ref":"#/components/schemas/HookHeader"},"type":"array"},"hook_id":{"format":"uuid","type":"string"},"id":{"format":"uuid","type":"string"},"method":{"type":"string"},"path":{"type":"string"},"received_at":{"format":"date-time","type":"string"},"remote_ip":{"description":"The sender's IP as seen at the edge.","type":["string","null"]}},"required":["id","hook_id","received_at","method","path","headers","content_type","body","body_is_utf8","body_size","remote_ip","expires_at"],"type":"object"},"HookRequestList":{"properties":{"requests":{"items":{"$ref":"#/components/schemas/HookRequestSummary"},"type":"array"}},"required":["requests"],"type":"object"},"HookRequestSummary":{"description":"Body metadata only — fetch the detail endpoint for headers and the body itself.","properties":{"body_size":{"description":"Body size in bytes as received.","type":"integer"},"content_type":{"type":["string","null"]},"hook_id":{"format":"uuid","type":"string"},"id":{"format":"uuid","type":"string"},"method":{"description":"Uppercased HTTP verb as received.","type":"string"},"path":{"description":"Path + query after the token; `/` when the request hit the bare ingest URL.","type":"string"},"received_at":{"format":"date-time","type":"string"}},"required":["id","hook_id","received_at","method","path","content_type","body_size"],"type":"object"},"Inbox":{"properties":{"created_at":{"format":"date-time","type":"string"},"domain":{"type":"string"},"email_address":{"description":"`local_part@domain` — the address to send to.","type":"string"},"id":{"format":"uuid","type":"string"},"local_part":{"type":"string"},"ttl_expires_at":{"format":"date-time","type":["string","null"]}},"required":["id","email_address","local_part","domain","ttl_expires_at","created_at"],"type":"object"},"InboxList":{"properties":{"inboxes":{"items":{"$ref":"#/components/schemas/InboxSummary"},"type":"array"}},"required":["inboxes"],"type":"object"},"InboxSummary":{"properties":{"created_at":{"format":"date-time","type":"string"},"fqdn":{"type":"string"},"id":{"format":"uuid","type":"string"},"local_part":{"type":"string"},"message_count":{"type":"integer"},"ttl_expires_at":{"format":"date-time","type":["string","null"]}},"required":["id","local_part","fqdn","ttl_expires_at","created_at","message_count"],"type":"object"},"LinkList":{"properties":{"links":{"items":{"$ref":"#/components/schemas/ExtractedLink"},"type":"array"}},"required":["links"],"type":"object"},"MessageDetail":{"properties":{"attachments":{"items":{"$ref":"#/components/schemas/AttachmentMeta"},"type":"array"},"expires_at":{"description":"Retention expiry (plan retention from receipt).","format":"date-time","type":"string"},"extracted":{"$ref":"#/components/schemas/Extracted"},"from_addr":{"type":"string"},"headers":{"description":"All headers in source order, folding unfolded.","items":{"$ref":"#/components/schemas/MessageHeader"},"type":"array"},"html_body":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"inbox_id":{"format":"uuid","type":"string"},"raw_size":{"description":"Size of the raw MIME in bytes.","type":"integer"},"received_at":{"format":"date-time","type":"string"},"subject":{"type":["string","null"]},"text_body":{"type":["string","null"]},"tls":{"type":"boolean"},"to_addrs":{"items":{"type":"string"},"type":"array"}},"required":["id","inbox_id","received_at","from_addr","to_addrs","subject","tls","text_body","html_body","headers","extracted","attachments","raw_size","expires_at"],"type":"object"},"MessageHeader":{"properties":{"name":{"type":"string"},"value":{"type":"string"}},"required":["name","value"],"type":"object"},"MessageList":{"properties":{"messages":{"items":{"$ref":"#/components/schemas/MessageSummary"},"type":"array"}},"required":["messages"],"type":"object"},"MessageSummary":{"properties":{"from_addr":{"type":"string"},"id":{"format":"uuid","type":"string"},"received_at":{"format":"date-time","type":"string"},"subject":{"type":["string","null"]},"tls":{"description":"Whether the delivering SMTP session had negotiated STARTTLS.","type":"boolean"},"to_addrs":{"items":{"type":"string"},"type":"array"}},"required":["id","received_at","from_addr","to_addrs","subject","tls"],"type":"object"},"OtpCandidate":{"properties":{"score":{"description":"Keyword-proximity score, one decimal.","type":"number"},"value":{"description":"The digit sequence.","type":"string"}},"required":["value","score"],"type":"object"},"PhoneNumber":{"properties":{"created_at":{"format":"date-time","type":"string"},"id":{"format":"uuid","type":"string"},"phone_number":{"description":"E.164, e.g. +15551234567.","type":"string"},"release_after":{"description":"Scheduled auto-release after a plan downgrade put the account over its number cap; null when not scheduled. Cleared if the account comes back within cap.","format":"date-time","type":["string","null"]},"status":{"description":"`pending` until the provider confirms the order.","enum":["pending","active"],"type":"string"}},"required":["id","phone_number","status","created_at","release_after"],"type":"object"},"PhoneNumberList":{"properties":{"phone_numbers":{"items":{"$ref":"#/components/schemas/PhoneNumber"},"type":"array"}},"required":["phone_numbers"],"type":"object"},"Problem":{"description":"RFC 7807 problem document — every error response uses this shape with `Content-Type: application/problem+json`.","properties":{"detail":{"description":"Human-readable specifics; null on 500.","type":["string","null"]},"status":{"description":"The HTTP status, duplicated in the body.","type":"integer"},"title":{"description":"Fixed short phrase for the status class, e.g. `bad request`, `not found`.","type":"string"},"type":{"const":"about:blank","type":"string"}},"required":["type","title","status","detail"],"type":"object"},"SmsDetail":{"properties":{"expires_at":{"format":"date-time","type":"string"},"extracted":{"$ref":"#/components/schemas/Extracted"},"from_number":{"type":"string"},"id":{"format":"uuid","type":"string"},"phone_number_id":{"format":"uuid","type":"string"},"received_at":{"format":"date-time","type":"string"},"text":{"type":"string"},"to_number":{"type":"string"}},"required":["id","phone_number_id","received_at","from_number","to_number","text","extracted","expires_at"],"type":"object"},"SmsList":{"properties":{"messages":{"items":{"$ref":"#/components/schemas/SmsSummary"},"type":"array"}},"required":["messages"],"type":"object"},"SmsSummary":{"description":"Unlike email summaries, SMS summaries carry the full `text` — the body is the message.","properties":{"from_number":{"type":"string"},"id":{"format":"uuid","type":"string"},"phone_number_id":{"format":"uuid","type":"string"},"received_at":{"format":"date-time","type":"string"},"text":{"type":"string"},"to_number":{"type":"string"}},"required":["id","phone_number_id","received_at","from_number","to_number","text"],"type":"object"},"SpamRule":{"properties":{"description":{"description":"Human-readable rule description, when spamd supplied one.","type":["string","null"]},"name":{"description":"SpamAssassin rule symbol, e.g. `MISSING_HEADERS`.","type":"string"},"score":{"description":"This rule's contribution to the total; may be negative.","type":"number"}},"required":["name","score"],"type":"object"},"SpfAuth":{"properties":{"domain":{"description":"The identity checked (MAIL FROM, falling back to HELO); may be empty.","type":"string"},"result":{"$ref":"#/components/schemas/AuthResult"}},"required":["result","domain"],"type":"object"},"Webhook":{"properties":{"consecutive_failures":{"type":"integer"},"created_at":{"format":"date-time","type":"string"},"description":{"type":["string","null"]},"disabled_at":{"format":"date-time","type":["string","null"]},"disabled_reason":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"secret_hint":{"description":"`whsec_…` plus the secret's last 4 chars.","type":"string"},"url":{"type":"string"}},"required":["id","url","description","secret_hint","consecutive_failures","disabled_at","disabled_reason","created_at"],"type":"object"},"WebhookCreated":{"properties":{"created_at":{"format":"date-time","type":"string"},"description":{"type":["string","null"]},"id":{"format":"uuid","type":"string"},"secret":{"description":"Signing secret (`whsec_…`) — shown exactly once; lists only carry a hint.","type":"string"},"url":{"type":"string"}},"required":["id","url","description","secret","created_at"],"type":"object"},"WebhookDelivery":{"properties":{"attempts":{"type":"integer"},"created_at":{"format":"date-time","type":"string"},"delivered_at":{"format":"date-time","type":["string","null"]},"event_type":{"description":"message.received, sms.received, domain.verified, or endpoint.test.","type":"string"},"id":{"description":"Delivery id — also the envelope's idempotency key and the X-MailFixture-Delivery header.","format":"uuid","type":"string"},"last_error":{"type":["string","null"]},"last_status_code":{"type":["integer","null"]},"next_attempt_at":{"format":"date-time","type":"string"},"status":{"enum":["pending","succeeded","failed"],"type":"string"}},"required":["id","event_type","status","attempts","next_attempt_at","last_status_code","last_error","created_at","delivered_at"],"type":"object"},"WebhookDeliveryList":{"properties":{"deliveries":{"items":{"$ref":"#/components/schemas/WebhookDelivery"},"type":"array"}},"required":["deliveries"],"type":"object"},"WebhookList":{"properties":{"webhooks":{"items":{"$ref":"#/components/schemas/Webhook"},"type":"array"}},"required":["webhooks"],"type":"object"},"WebhookTestAccepted":{"properties":{"delivery_id":{"format":"uuid","type":"string"}},"required":["delivery_id"],"type":"object"}},"securitySchemes":{"apiKey":{"description":"mailfixture API key (`mfx_…`) as a Bearer token. Create one in the dashboard (/app/keys) or via POST /v1/keys.","scheme":"bearer","type":"http"}}},"externalDocs":{"description":"Hand-written API reference","url":"https://mailfixture.com/docs/api"},"info":{"contact":{"email":"support@mailfixture.com","name":"mailfixture support"},"description":"REST API for mailfixture: create inboxes, trigger your app's email flow, then read messages back and assert on extracted OTPs, links, SPF/DKIM/DMARC verdicts, and a SpamAssassin score. Paid plans add dedicated US phone numbers with the same loop for SMS.\n\nConventions:\n- Every request needs `Authorization: Bearer mfx_<key>`.\n- Errors are RFC 7807 `application/problem+json` (`{type, title, status, detail}`); `type` is always `about:blank`. 404 uniformly covers both missing and not-owned resources.\n- Rate limiting is a per-key token bucket: 2 requests/second sustained, burst 60. Refusals are 429 with a `Retry-After` header (integer seconds). Inbox, phone-number, and capture-hook creation draw on a shared second per-key bucket (burst 10, refills 10/minute).\n- Timestamps are RFC 3339 UTC strings; ids are UUIDs. Nullable fields serialize as JSON `null`, never omitted (except where a schema notes otherwise).\n- List responses wrap the array in an object key (`{\"messages\": [...]}`) and are not paginated; message lists take a `since` cursor instead.\n- Long-polling: message list endpoints accept `wait` (clamped to 60 s) and hold the request until a match arrives or the window closes. An empty window is a 200 with an empty array, never 204 or 408.","summary":"Programmatic email inboxes (and SMS numbers) for automated testing. Receive-only: mailfixture never sends.","termsOfService":"https://mailfixture.com/terms","title":"mailfixture API","version":"0.1.0"},"openapi":"3.1.0","paths":{"/v1/domains":{"get":{"description":"The account's custom domains plus the shared utility domains. `setup` is present only on unverified custom domains.","operationId":"list_domains","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainList"}}},"description":"Available domains."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List domains","tags":["domains"]},"post":{"description":"Registers a custom receiving domain. The response's `setup` object carries the exact TXT challenge and MX records to publish; mail is accepted only after verification. Newly created domains are auto-rechecked every 60 s for their first 7 days.","operationId":"create_domain","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDomainRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"}}},"description":"Domain registered, verification pending."},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"`fqdn` is not a valid domain name."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Custom domains require a paid plan."},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"That domain is already registered."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Add a custom domain","tags":["domains"]}},"/v1/domains/{id}":{"delete":{"operationId":"delete_domain","parameters":[{"description":"Domain id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Domain deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such custom domain on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Delete a custom domain","tags":["domains"]}},"/v1/domains/{id}/verify":{"post":{"description":"Runs live DNS TXT + MX checks (fresh, uncached resolution) and persists the result. Always 200 — failed checks are reported in `txt_ok`/`mx_ok`, never as an error status.","operationId":"verify_domain","parameters":[{"description":"Domain id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DomainVerifyResult"}}},"description":"Check results and current verification state."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such custom domain on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Run domain verification now","tags":["domains"]}},"/v1/hook-requests/{id}":{"get":{"operationId":"get_hook_request","parameters":[{"description":"Captured request id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HookRequestDetail"}}},"description":"The captured request with headers and body."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such captured request on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Get a captured request","tags":["hooks"]}},"/v1/hooks":{"get":{"operationId":"list_hooks","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HookList"}}},"description":"The account's live hooks."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List capture hooks","tags":["hooks"]},"post":{"description":"Mints a public ingest URL (on a dedicated utility host, never the brand or API origin). Any HTTP request sent there is recorded and answered with a fixed `200 {\"ok\":true}` — responses are not configurable. Creation shares the burst-10 creation bucket with inboxes. Captured requests are hard-capped per plan (free 100/mo, solo 2,500, team 15,000, scale 100,000; daily caps 100/2,000/10,000/50,000) — over-cap ingest answers 429 with Retry-After. Captures never fire outbound webhook events (loop prevention, by design).","operationId":"create_hook","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateHookRequest"}}},"required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Hook"}}},"description":"Hook created; `ingest_url` is the address to give the app under test."},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"`label` over 200 chars or `ttl_seconds` out of range."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Account frozen, or the plan's hook cap is reached (free 1, solo 3, team 10, scale 25)."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Create a capture hook","tags":["hooks"]}},"/v1/hooks/{id}":{"delete":{"description":"Deletes the hook and all captured requests immediately; the ingest URL answers 404 afterwards.","operationId":"delete_hook","parameters":[{"description":"Hook id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Hook deleted; its captured requests are gone."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such hook on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Delete a capture hook","tags":["hooks"]}},"/v1/hooks/{id}/clear":{"post":{"description":"Deletes the hook's captured requests but keeps the hook and its ingest URL — rerun hygiene between test runs.","operationId":"clear_hook","parameters":[{"description":"Hook id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Captured requests dropped; the hook remains."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such hook on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Clear captured requests","tags":["hooks"]}},"/v1/hooks/{id}/requests":{"get":{"description":"Summaries ordered oldest-first by `received_at`, at most 100 per response; bodies stay on the detail endpoint. Long-poll semantics match the email endpoint.","operationId":"list_hook_requests","parameters":[{"description":"Hook id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}},{"$ref":"#/components/parameters/Since"},{"description":"Filter expression: `method:<verb>` (exact, case-insensitive), `path:<term>` or `body:<term>` (case-insensitive substring); a bare term (or unknown prefix) matches the body. Body matching scans the first 32 KiB of body bytes; detail responses still return the complete body.","in":"query","name":"match","required":false,"schema":{"type":"string"}},{"$ref":"#/components/parameters/Wait"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HookRequestList"}}},"description":"Matching captured requests — empty array when nothing matched within the wait window."},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"`match` has an empty term after its field prefix."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such hook on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List or long-poll captured requests","tags":["hooks"]}},"/v1/inboxes":{"get":{"description":"Newest first. At most 500 inboxes are returned; there is no pagination.","operationId":"list_inboxes","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboxList"}}},"description":"The account's inboxes."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List inboxes","tags":["inboxes"]},"post":{"description":"Creates a receiving address. The body is optional: with no body the inbox gets a random 10-character local part on the shared domain and no expiry. Creation draws on a second per-key bucket (burst 10, refills 10/minute) in addition to the general request budget — either can answer 429.","operationId":"create_inbox","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInboxRequest"}}},"required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Inbox"}}},"description":"Inbox created."},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Invalid `local_part` (1–64 chars of [a-z0-9._-], must not start or end with a dot) or `ttl_seconds` out of range (60–2592000)."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Account frozen, or the plan's active-inbox cap is reached (free 25, solo 250, team 1000, scale 5000)."},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"`domain` names a domain that is neither shared nor owned by this account."},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"That address already exists."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No shared domain is configured on this deployment (only when `domain` was omitted)."}},"summary":"Create an inbox","tags":["inboxes"]}},"/v1/inboxes/{id}":{"delete":{"description":"Deletes the inbox and all of its messages immediately — there is no history-until-retention.","operationId":"delete_inbox","parameters":[{"description":"Inbox id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Inbox and its messages deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such inbox on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Delete an inbox","tags":["inboxes"]}},"/v1/inboxes/{id}/clear":{"post":{"description":"Deletes all messages but keeps the inbox and its address. Idempotent — clearing an empty inbox is still 204.","operationId":"clear_inbox","parameters":[{"description":"Inbox id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Messages deleted; inbox kept."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such inbox on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Clear an inbox","tags":["inboxes"]}},"/v1/inboxes/{id}/messages":{"get":{"description":"Summaries ordered oldest-first by `received_at`, at most 100 per response. With `wait` > 0 the request holds until a matching message arrives (LISTEN/NOTIFY wake, typically < 2 s after SMTP accept) or the window closes.","operationId":"list_messages","parameters":[{"description":"Inbox id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}},{"$ref":"#/components/parameters/Since"},{"description":"Filter expression: `subject:<term>`, `from:<term>`, or `to:<term>` select a field; any other input — including a bare term — matches against the subject. Case-insensitive substring match; LIKE wildcards in the term are escaped. `to:` matches any recipient.","in":"query","name":"match","required":false,"schema":{"type":"string"}},{"$ref":"#/components/parameters/Wait"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageList"}}},"description":"Matching messages — empty array when nothing matched within the wait window."},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"`match` has an empty term after its field prefix."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such inbox on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List or long-poll messages","tags":["messages"]}},"/v1/keys":{"get":{"description":"Newest first; revoked keys are included (non-null `revoked_at`). Secrets are never returned.","operationId":"list_keys","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyList"}}},"description":"The account's keys."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List API keys","tags":["keys"]},"post":{"description":"Mints a new key. The full secret is revealed once, in this response only — only its argon2id hash is stored.","operationId":"create_key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}},"required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"}}},"description":"The new key — save `key` now; it cannot be retrieved again."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Create an API key","tags":["keys"]}},"/v1/keys/{id}":{"delete":{"description":"Soft revoke: the key stops authenticating immediately but stays listed with its `revoked_at`. A key may revoke itself.","operationId":"revoke_key","parameters":[{"description":"Key id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Key revoked."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such active key on this account — already-revoked keys 404 too."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Revoke an API key","tags":["keys"]}},"/v1/messages/{id}":{"get":{"description":"Full detail: bodies, unfolded headers, extraction results, and attachment metadata. Raw MIME bytes are not included; use the attachment download endpoint for decoded parts.","operationId":"get_message","parameters":[{"description":"Message id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageDetail"}}},"description":"The message."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such message on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Get a message","tags":["messages"]}},"/v1/messages/{id}/attachments/{index}":{"get":{"description":"Decoded bytes of one attachment, served as a forced download: `Content-Type` is always `application/octet-stream` and disposition is always `attachment` regardless of the declared type (emails are hostile input). The filename is sanitized to `[A-Za-z0-9. _-]`, 80 chars max.","operationId":"download_attachment","parameters":[{"description":"Message id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}},{"description":"Zero-based position in the message's `attachments` metadata array.","in":"path","name":"index","required":true,"schema":{"minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"application/octet-stream":{"schema":{"format":"binary","type":"string"}}},"description":"The decoded attachment bytes.","headers":{"Content-Disposition":{"description":"`attachment; filename=\"<sanitized>\"` — always a download.","schema":{"type":"string"}},"X-Content-Type-Options":{"description":"Always `nosniff`.","schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such message, no servable attachment at that index (nested message/multipart parts are not servable), or the offloaded raw MIME object no longer exists."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"The raw MIME was offloaded to object storage and the store is unavailable or not configured."}},"summary":"Download an attachment","tags":["messages"]}},"/v1/messages/{id}/links":{"get":{"description":"Every URL from the HTML and text parts, with anchor text and a class guess.","operationId":"get_message_links","parameters":[{"description":"Message id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkList"}}},"description":"Classified links; empty array when none were extracted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such message on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Get the extracted links","tags":["messages"]}},"/v1/messages/{id}/links/follow":{"post":{"description":"GETs one of the message's extracted links (default: the first `verify`-classed link), following up to 5 redirects with every hop re-validated against the SSRF guard (https-only, public hosts; 5 s per hop, 15 s total). The target's body is never returned. The final hop's HTTP status is reported in the outcome, not judged — a remote 500 is still a 200 here with `status: 500`.","operationId":"follow_message_link","parameters":[{"description":"Message id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowRequest"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowOutcome"}}},"description":"The follow outcome."},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"More than one of `url`/`class`/`index` was passed, or the selected link itself is not followable (guard-refused: non-https, private host, …)."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such message, or no extracted link matches the selector — the detail lists the link classes that do exist."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"},"502":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"The remote misbehaved: unreachable, a redirect hop was guard-refused, more than 5 redirects, or the chain exceeded 15 seconds."}},"summary":"Follow an extracted link server-side","tags":["messages"]}},"/v1/messages/{id}/otp":{"get":{"description":"The message's OTP verdict. `best` is null when no candidate scores above the confidence threshold — honesty over guessing. Always this shape, even for messages with no candidates.","operationId":"get_message_otp","parameters":[{"description":"Message id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractedOtp"}}},"description":"Ranked OTP candidates and the best pick."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such message on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Get the extracted OTP","tags":["messages"]}},"/v1/phone-numbers":{"get":{"operationId":"list_phone_numbers","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneNumberList"}}},"description":"The account's numbers."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List phone numbers","tags":["phone-numbers"]},"post":{"description":"Provisions a dedicated US number — no request body, US local is the only offering. Paid plans only. Numbers start `pending` and flip to `active` once the provider confirms (usually seconds). Creation shares the burst-10 creation bucket with inboxes.","operationId":"create_phone_number","responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneNumber"}}},"description":"Number ordered."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Account frozen, phone numbers require a paid plan (the free cap is 0), or the plan's number cap is reached (solo 1, team 3, scale 10)."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"SMS is not configured on this deployment, or provisioning failed at the provider — safe to retry."}},"summary":"Provision a phone number","tags":["phone-numbers"]}},"/v1/phone-numbers/{id}":{"delete":{"description":"Releases the number at the provider first, then deletes it and all of its texts immediately. A provider failure leaves the number owned so the call can be retried.","operationId":"release_phone_number","parameters":[{"description":"Phone number id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Number released; its texts are gone."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such phone number on this account."},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"The number order is still pending — wait for it to become active before releasing."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Release failed at the provider; try again shortly."}},"summary":"Release a phone number","tags":["phone-numbers"]}},"/v1/phone-numbers/{id}/messages":{"get":{"description":"Summaries ordered oldest-first by `received_at`, at most 100 per response; summaries include the full `text` (the body is the message). Long-poll semantics match the email endpoint.","operationId":"list_sms","parameters":[{"description":"Phone number id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}},{"$ref":"#/components/parameters/Since"},{"description":"Filter expression: `from:<term>`, `to:<term>`, or `body:<term>`; a bare term (or unknown prefix) matches the body — SMS has no subject. Case-insensitive substring match.","in":"query","name":"match","required":false,"schema":{"type":"string"}},{"$ref":"#/components/parameters/Wait"}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmsList"}}},"description":"Matching texts — empty array when nothing matched within the wait window."},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"`match` has an empty term after its field prefix."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such phone number on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List or long-poll texts","tags":["sms"]}},"/v1/sms/{id}":{"get":{"operationId":"get_sms","parameters":[{"description":"SMS id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmsDetail"}}},"description":"The text with its extraction results."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such SMS on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Get a text","tags":["sms"]}},"/v1/sms/{id}/links":{"get":{"description":"Byte-identical shape to the email links endpoint.","operationId":"get_sms_links","parameters":[{"description":"SMS id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkList"}}},"description":"Classified links; empty array when none were extracted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such SMS on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Get the extracted links","tags":["sms"]}},"/v1/sms/{id}/links/follow":{"post":{"description":"The email follow endpoint's SMS twin: same selectors, same SSRF-guarded follow, same outcome shape.","operationId":"follow_sms_link","parameters":[{"description":"SMS id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowRequest"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FollowOutcome"}}},"description":"The follow outcome."},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"More than one of `url`/`class`/`index` was passed, or the selected link itself is not followable."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such SMS, or no extracted link matches the selector."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"},"502":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"The remote misbehaved: unreachable, a redirect hop was guard-refused, more than 5 redirects, or the chain exceeded 15 seconds."}},"summary":"Follow an extracted link server-side","tags":["sms"]}},"/v1/sms/{id}/otp":{"get":{"description":"Byte-identical shape to the email OTP endpoint.","operationId":"get_sms_otp","parameters":[{"description":"SMS id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractedOtp"}}},"description":"Ranked OTP candidates and the best pick."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such SMS on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Get the extracted OTP","tags":["sms"]}},"/v1/webhooks":{"get":{"operationId":"list_webhooks","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookList"}}},"description":"The account's endpoints, secrets masked to a hint."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List webhook endpoints","tags":["webhooks"]},"post":{"description":"Registers an HTTPS endpoint. Endpoints receive every event type (`message.received`, `sms.received`, `domain.verified`, `endpoint.test`) — there is no per-endpoint event filter — and payloads never contain message bodies. Deliveries are signed `X-MailFixture-Signature: t=<unix>,v1=<hmac-sha256>` with the endpoint's secret; 8 attempts with backoff, and 5 consecutively exhausted deliveries auto-disable the endpoint.","operationId":"create_webhook","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreated"}}},"description":"Endpoint created — save `secret` now; lists only ever show a hint."},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"URL refused: not https, credentials or whitespace in the URL, unresolvable host, or the host is not a public IP."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Webhooks require a paid plan, or the plan's endpoint cap is reached (solo 3, team 10, scale 25)."},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"An endpoint with this URL already exists."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Create a webhook endpoint","tags":["webhooks"]}},"/v1/webhooks/{id}":{"delete":{"operationId":"delete_webhook","parameters":[{"description":"Endpoint id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Endpoint deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such webhook endpoint on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Delete a webhook endpoint","tags":["webhooks"]}},"/v1/webhooks/{id}/deliveries":{"get":{"description":"Newest first. Finished deliveries are kept 7 days; pending ones are never purged.","operationId":"list_webhook_deliveries","parameters":[{"description":"Endpoint id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}},{"description":"Rows to return, clamped to 1–200.","in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":200,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryList"}}},"description":"Recent deliveries."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such webhook endpoint on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"List delivery history","tags":["webhooks"]}},"/v1/webhooks/{id}/enable":{"post":{"description":"Clears the disabled state and resets the consecutive-failure counter.","operationId":"enable_webhook","parameters":[{"description":"Endpoint id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Endpoint re-enabled."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such webhook endpoint on this account."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Re-enable a disabled endpoint","tags":["webhooks"]}},"/v1/webhooks/{id}/test":{"post":{"description":"Enqueues an `endpoint.test` delivery through the normal signed pipeline.","operationId":"test_webhook","parameters":[{"description":"Endpoint id.","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestAccepted"}}},"description":"Test delivery enqueued."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"No such webhook endpoint on this account."},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"The endpoint is disabled — enable it first."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/ServerError"}},"summary":"Send a test event","tags":["webhooks"]}}},"security":[{"apiKey":[]}],"servers":[{"description":"Canonical API origin","url":"https://api.mailfixture.com"}],"tags":[{"description":"Receiving addresses. Deleting an inbox drops its messages immediately.","name":"inboxes"},{"description":"Received email: long-poll lists, full detail, extraction views, attachment downloads, server-side link following.","name":"messages"},{"description":"Dedicated US numbers for inbound SMS (paid plans only; receive-only).","name":"phone-numbers"},{"description":"Received texts — extraction shapes are byte-identical to email.","name":"sms"},{"description":"Custom receiving domains (TXT + MX verification) plus the shared utility domains.","name":"domains"},{"description":"API keys. Secrets are argon2id-hashed at rest and revealed once at creation.","name":"keys"},{"description":"Signed HTTPS event delivery (paid plans). Endpoints receive every event type; payloads never contain message bodies.","name":"webhooks"},{"description":"Capture hooks (webhook capture): public ingest URLs that record every HTTP request sent to them, for asserting the app under test fired its webhooks. Receive-only — captures are never forwarded, replayed, or turned into events. Not to be confused with the outbound `webhooks` resource.","name":"hooks"}]}