{"openapi":"3.1.0","info":{"title":"Flyanote API","version":"main-fc18351"},"servers":[{"url":"https://app.flyanote.com/api"}],"webhooks":{},"paths":{"/health":{"get":{"operationId":"getHealth","summary":"Liveness, with the deployed version and commit","responses":{"200":{"description":"The API is up","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"500":{"description":"Internal error (`internal`)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/ping":{"get":{"operationId":"getPing","summary":"Reads the pipeline probe row through the database, RLS, and tenant context","description":"Each API instance reuses its last answer for 15 seconds: a success from when it arrived, a failure from when its probe began.","responses":{"200":{"description":"The database path works","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ping"}}}},"500":{"description":"Internal error (`internal`)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Database unavailable (`database_unavailable`)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/openapi":{"get":{"operationId":"getOpenApi","summary":"This OpenAPI document, as the running release serves it","description":"Built from the routes this deployment mounts, so it describes exactly what the API answers here: `info.version` names the release, `servers` this deployment's address, and an operation that isn't live here yet isn't listed. The web app's own operations are left out. Cacheable for five minutes, readable from any origin, and gzipped for a client that accepts it.","responses":{"200":{"description":"The OpenAPI 3.1 document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenApiDocument"}}}},"500":{"description":"Internal error (`internal`)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/account/profile":{"get":{"summary":"The account's profile","operationId":"getAccountProfile","tags":["public"],"security":[{"apiKey":["account:read"]}],"responses":{"200":{"description":"The profile","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountProfile"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"patch":{"summary":"Change the account's profile (owners and admins; keys need `account:write`, live only)","description":"Changes the fields the body names and leaves the rest. Sending values the profile already has changes nothing, so a retry is safe, except that the first save records `set_at`.","operationId":"updateAccountProfile","tags":["public"],"security":[{"apiKey":["account:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountProfileChange"}}}},"responses":{"200":{"description":"The profile as it now is","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountProfile"}}}},"400":{"description":"Malformed request (`malformed_request`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`); Test keys can't do this (`test_key_not_allowed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"413":{"description":"Request body too large (`payload_too_large`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"415":{"description":"Unsupported media type (`unsupported_media_type`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failed (`validation_failed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/sender-profile":{"get":{"summary":"The account's sender profile","operationId":"getSenderProfile","tags":["public"],"security":[{"apiKey":["sender_profile:read"]}],"responses":{"200":{"description":"The current version","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderProfile"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Sender profile not set (`sender_profile_not_set`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"put":{"summary":"Set the account's sender profile (owners and admins; keys need `sender_profile:write`, live only)","description":"Replaces the whole profile. A change makes a new version, and the earlier one is kept. Sending the profile as it already is changes nothing, so a retry is safe, and it spends nothing of the account's `sender-profile-change` limit.","operationId":"setSenderProfile","tags":["public"],"security":[{"apiKey":["sender_profile:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderProfileChange"}}}},"responses":{"200":{"description":"The profile as it now is","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderProfile"}}}},"400":{"description":"Malformed request (`malformed_request`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`); Test keys can't do this (`test_key_not_allowed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"413":{"description":"Request body too large (`payload_too_large`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"415":{"description":"Unsupported media type (`unsupported_media_type`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failed (`validation_failed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/api-keys/current":{"get":{"operationId":"getCurrentApiKey","summary":"The API key making this request","description":"Which account the key acts in, whether it is a `live` or `test` key, and what it may do. It needs no scope, so any working key can check itself before it is used for anything else.","tags":["public"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"The key","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurrentApiKey"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/domains":{"post":{"operationId":"createDomain","summary":"Add a sending domain","description":"Returns the two CNAME records to add at your DNS host. Flyanote checks for them every few minutes for the first hour, then hourly, for up to 3 days: once they point at Flyanote it sets the domain up behind them (`setup.status` turns `ready`), and the domain verifies within minutes after that. Adding a domain the account already has returns it with 200, so a retry is safe.","tags":["public"],"security":[{"apiKey":["domains:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","minLength":1,"maxLength":1024,"description":"The domain you'll send from: your organization's own, not a free mailbox provider's. Adding one you already have returns it.","example":"example.com"}},"required":["domain"],"additionalProperties":false}}}},"responses":{"200":{"description":"The domain, which the account already had","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"}}}},"201":{"description":"The domain, added","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"}}}},"400":{"description":"Malformed request (`malformed_request`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`); Test keys can't do this (`test_key_not_allowed`); Account paused (`account_paused`); Not one of this key's domains (`api_key_domain_not_allowed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Too many domains (`domain_limit_reached`); Too many domains waiting to verify (`domain_pending_limit_reached`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"413":{"description":"Request body too large (`payload_too_large`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"415":{"description":"Unsupported media type (`unsupported_media_type`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Not a usable domain (`domain_invalid`); A freemail domain can't be used (`domain_freemail`); Flyanote's own domains can't be added (`domain_reserved`); Only test domains here (`domain_test_only`); Validation failed (`validation_failed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`); Too many domain changes (`domain_changes_throttled`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`); Domains unavailable (`domains_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"operationId":"listDomains","summary":"The account's sending domains","tags":["public"],"security":[{"apiKey":["domains:read"]}],"responses":{"200":{"description":"Every domain, newest first","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Domain"}}},"required":["data"]}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`); Domains unavailable (`domains_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/domains/{id}":{"get":{"operationId":"getDomain","summary":"A sending domain, with the DNS records to add","tags":["public"],"security":[{"apiKey":["domains:read"]}],"parameters":[{"schema":{"type":"string","maxLength":64,"example":"dom_01j9zq2k3m4n5p6q7r8s9t0v1w"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The domain","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Domain not found (`domain_not_found`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failed (`validation_failed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`); Domains unavailable (`domains_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"delete":{"operationId":"deleteDomain","summary":"Remove a sending domain","description":"Stops sending from the domain, and removes Flyanote's side of its setup. The CNAMEs at your DNS host can stay: adding the domain back reuses them. A Flyanote address is released only in the app, since its name is used up for good (403 `address_app_only` for an API key).","tags":["public"],"security":[{"apiKey":["domains:write"]}],"parameters":[{"schema":{"type":"string","maxLength":64,"example":"dom_01j9zq2k3m4n5p6q7r8s9t0v1w"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The domain, removed","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`); Test keys can't do this (`test_key_not_allowed`); Account paused (`account_paused`); Only in the app (`address_app_only`); Not one of this key's domains (`api_key_domain_not_allowed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Domain not found (`domain_not_found`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failed (`validation_failed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`); Too many domain changes (`domain_changes_throttled`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`); Domains unavailable (`domains_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/domains/{id}/check":{"post":{"operationId":"checkDomain","summary":"Check a sending domain's records again","description":"Checks the domain's DNS records again now, rather than at its next scheduled check, and for a domain that stopped checking after 3 days, starts 3 more days of checks. Per domain, the waits between checks grow with the checks asked for in the last hour: 30 seconds, 30 seconds, 1 minute, 2 minutes, 5 minutes, then 10 minutes. Sooner, nothing more is queued, and `verification.check_again_at` says when. An account may ask 12 times an hour in all (429 `domain_checks_throttled`). The result shows up on the domain within a minute or so.","tags":["public"],"security":[{"apiKey":["domains:write"]}],"parameters":[{"schema":{"type":"string","maxLength":64,"example":"dom_01j9zq2k3m4n5p6q7r8s9t0v1w"},"required":true,"name":"id","in":"path"}],"responses":{"202":{"description":"The domain, with its check queued, or when the next may be asked for","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Domain"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`); Test keys can't do this (`test_key_not_allowed`); Not one of this key's domains (`api_key_domain_not_allowed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Domain not found (`domain_not_found`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failed (`validation_failed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`); Too many domain checks (`domain_checks_throttled`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`); Domains unavailable (`domains_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/account/balance":{"get":{"operationId":"getAccountBalance","summary":"The key's account's credit balance","description":"What the account can spend now, so an agent or a script can check before it sends. A `fan_test_` key reads the same balance: test sends are free, and never use it.","tags":["public"],"security":[{"apiKey":["billing:read"]}],"responses":{"200":{"description":"The balance","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditBalance"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/messages":{"post":{"operationId":"sendMessage","summary":"Send a transactional message","description":"Accepts one message to one recipient and answers at once with its id; Flyanote sends it within seconds. Poll `GET /v1/messages/{id}` for what happened. A test key (`fan_test_…`) never delivers and costs nothing. It runs every check but one: it's never refused for an address on Flyanote's own abuse list, so a passing test send isn't proof the live one will go. Always send an `Idempotency-Key`, and reuse it when you retry: a retry never sends twice. The request body can be up to 2 MiB.","tags":["public"],"security":[{"apiKey":["messages:send"]}],"parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7e]{1,255}$","description":"Required. A value unique to this message, like a UUID or your own order id. Once a request with it is accepted, a retry with the same key and body within 24 hours returns that 202 instead of sending again, and the same key with a different body is refused with 409. Retry any 5xx, or a timeout, with the same key and the same body: the message may have been accepted before the error. A 4xx kept nothing new: fix the request and retry (after a 409, the key already names another message, so use a new one).","example":"5f0c7c9e-8f4b-4b7e-9a57-2f1d0c6e4a11"},"required":true,"description":"Required. A value unique to this message, like a UUID or your own order id. Once a request with it is accepted, a retry with the same key and body within 24 hours returns that 202 instead of sending again, and the same key with a different body is refused with 409. Retry any 5xx, or a timeout, with the same key and the same body: the message may have been accepted before the error. A 4xx kept nothing new: fix the request and retry (after a 409, the key already names another message, so use a new one).","name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessage"}}}},"responses":{"202":{"description":"Accepted","headers":{"idempotent-replayed":{"schema":{"type":"string","description":"`true` when this response replays an earlier request's with the same key."},"required":false,"description":"`true` when this response replays an earlier request's with the same key."},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageAccepted"}}}},"400":{"description":"Malformed request (`malformed_request`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"402":{"description":"Insufficient credit (`insufficient_credit`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`); Account paused (`account_paused`); Not one of this key's domains (`api_key_domain_not_allowed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Template not found (`template_not_found`); Template version not found (`template_version_not_found`); Rendering isn't available here (`rendering_not_configured`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Idempotency key already used (`idempotency_key_reused`); Template archived (`template_archived`); Template not published (`template_not_published`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"413":{"description":"Request body too large (`payload_too_large`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"415":{"description":"Unsupported media type (`unsupported_media_type`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Sender not ready (`sender_not_ready`); Reply-to not confirmed (`reply_to_unverified`); Not a transactional template (`template_category_not_allowed`); Template couldn't be rendered (`template_render_failed`); Message too large (`message_too_large`); Validation failed (`validation_failed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`); No price in effect (`pricing_unavailable`); Sending unavailable (`sending_unavailable`); Rendering unavailable (`rendering_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"get":{"operationId":"listMessages","summary":"Messages, newest first","tags":["public"],"security":[{"apiKey":["messages:read"]}],"parameters":[{"schema":{"type":"string","maxLength":64,"description":"The previous page’s `next_cursor`."},"required":false,"description":"The previous page’s `next_cursor`.","name":"cursor","in":"query"},{"schema":{"type":"string","enum":["queued","sending","sent","delivered","bounced","complained","failed","suppressed","test"],"description":"Only messages with this status."},"required":false,"description":"Only messages with this status.","name":"status","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Only messages created after this time (RFC 3339)."},"required":false,"description":"Only messages created after this time (RFC 3339).","name":"created_after","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Only messages created before this time (RFC 3339)."},"required":false,"description":"Only messages created before this time (RFC 3339).","name":"created_before","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":25,"example":25},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"A page of messages","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"next_cursor":{"type":["string","null"],"description":"Null on the last page."}},"required":["data","next_cursor"]}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failed (`validation_failed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/messages/{id}":{"get":{"operationId":"getMessage","summary":"A message, with what happened to it","tags":["public"],"security":[{"apiKey":["messages:read"]}],"parameters":[{"schema":{"type":"string","maxLength":64,"example":"msg_01j9zq2k3m4n5p6q7r8s9t0v1w"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The message","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageWithEvents"}}}},"401":{"description":"API key missing (`api_key_missing`); Invalid API key (`api_key_invalid`); API key revoked (`api_key_revoked`); API key expired (`api_key_expired`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Account closed (`account_closed`); Missing scope (`api_key_scope_missing`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Message not found (`message_not_found`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Validation failed (`validation_failed`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Too many requests (`rate_limited`)","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal error (`internal`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"API keys unavailable (`api_keys_unavailable`)","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"fan_live_… or fan_test_…","description":"An API key (ADR-0009), sent as `Authorization: Bearer fan_live_…`. Test keys (`fan_test_…`) run everything but never send real mail, and run every check but one: a test send is never refused for an address on Flyanote's own abuse list, so a test send that passes doesn't prove the live one will. Each operation lists the scope it needs."}},"headers":{"RateLimit":{"description":"What is left under each limit that applies (IETF draft-ietf-httpapi-ratelimit-headers-11): `r` requests now, and `t` seconds until the limit is full again, or on a 429 until this request would fit. For `render-work`, `r` is KiB of document, and a 429 can leave some: fewer than the document needs. Sent on every response once the API key or session is known, so not on a request refused before that (a 401, or a 413 for a body too large). Absent when the limits could not be checked.","schema":{"type":"string"},"example":"\"general-key\";r=97;t=1, \"general-account\";r=190;t=1"},"RateLimit-Policy":{"description":"Each limit that applies to this request: `q` requests per `w` seconds, as a bucket that holds `q` and refills evenly. A key has its own limit, and its account has one across all its keys; a signed-in user has one across their accounts. Some changes also count toward a limit of the account's own, whoever makes them (`template-publish`, `template-delete`, `brand-kit-change`, `sender-profile-change`), stated once the change is allowed. `sender-profile-change` has one share for the account's keys and another for its members, each at the stated rate. Validate and render also count toward the account's `render-work` in each mode, by the KiB of the document they compile, stated once the document is read: there `q` and `r` are KiB. While the limits can't be checked, `render-work` lets only documents of at most 64 KiB through, and answers a larger one 429 `rate_limited` with `Retry-After`. Sent once the API key or session is known.","schema":{"type":"string"},"example":"\"general-key\";q=100;w=5, \"general-account\";q=200;w=5"},"Retry-After":{"description":"Seconds to wait before retrying (RFC 9110). The body says the same in `retry_after`.","schema":{"type":"integer","minimum":1},"example":3}},"schemas":{"Health":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"version":{"type":"string"},"sha":{"type":"string"},"region":{"type":"string"}},"required":["status","version","sha","region"]},"Problem":{"type":"object","properties":{"type":{"type":"string","description":"A URI that identifies the kind of problem. It never changes for a given `code`.","example":"https://flyanote.com/problems/validation_failed"},"title":{"type":"string","description":"A short summary of the kind of problem.","example":"Validation failed"},"status":{"type":"integer","description":"The response's HTTP status code.","example":422},"detail":{"type":"string","description":"What went wrong in this occurrence."},"code":{"type":"string","description":"A stable, machine-readable code.","example":"validation_failed"},"remediation":{"type":"array","items":{"$ref":"#/components/schemas/RemediationStep"},"description":"Ordered steps a person or an agent can take to fix the problem."},"docs_url":{"type":"string","description":"Where this code is explained in the docs.","example":"https://flyanote.com/docs/errors#validation-failed"},"request_id":{"type":"string","description":"Quote this when contacting support. Also the `x-request-id` header."},"errors":{"type":"array","items":{"$ref":"#/components/schemas/FieldError"},"description":"Each invalid value: `validation_failed`, and the codes that say so in `detail` (an email document, a brand kit, or a JSON Patch that was refused)."},"findings":{"type":"array","items":{"$ref":"#/components/schemas/Finding"},"description":"What the content checks found, when that's why the request was refused (`template_checks_failed`)."},"retry_after":{"type":"integer","description":"Seconds to wait before retrying, as in the `Retry-After` header (a 429 only).","example":3}},"required":["type","title","status","detail","code","remediation","docs_url","request_id"],"description":"Every error, as RFC 9457 problem+json. One exception: a 429 from the edge's per-IP limit, before the API, has the same `code` and fields, plus `retryable`, but no `request_id`, and its `content-type` is `application/json`."},"RemediationStep":{"type":"object","properties":{"action":{"type":"string","description":"What to do.","example":"Retry after a few seconds."},"operation_id":{"type":"string","description":"The `operationId` (in the API’s OpenAPI document) of the first public API operation `action` names, when it names one. `action` says its method and path too. Follow the whole `action`, with any step it says comes first: the operation may be only part of it.","example":"listDomains"}},"required":["action"]},"FieldError":{"type":"object","properties":{"in":{"type":"string","enum":["body","query","path","header","cookie"],"description":"The part of the request."},"pointer":{"type":"string","description":"A JSON Pointer (RFC 6901) to the invalid value within `in`. Empty means all of it.","example":"/to/0"},"detail":{"type":"string","description":"What's wrong with the value.","example":"Invalid email address"},"code":{"type":"string","description":"A stable code for the kind of problem, where the operation has its own: an email document's or a brand kit's (`document_invalid`, `brand_kit_invalid`), or why a JSON Patch operation failed (`patch_failed`)."},"line":{"type":"integer","exclusiveMinimum":0,"description":"For a refusal inside a custom HTML string, its Liquid included (`document_invalid`): the line within the string at `pointer`, 1-based, with lines broken at `\\n`.","example":3},"column":{"type":"integer","exclusiveMinimum":0,"description":"With `line`: the column within that line, 1-based, in UTF-16 code units, as editors count.","example":12},"docs_url":{"type":"string","description":"Where `code` is explained, for an email document's or a brand kit's errors: its entry in the email format reference.","example":"https://flyanote.com/docs/email-format#color-invalid"},"message_id":{"type":"string","description":"For an email document's or a brand kit's problems: the key of `detail`'s message in the message catalog Flyanote's app translates (not yet published), so a client can show it in its user's language, with `params`. Without it, show `detail`. With `line`, `detail` is `emailSchema.position.at` around this message, given `line`, `column`, and this message as `message`. Not a finding's `message_key`, which is the health checks' own.","example":"emailSchema.string.atMost"},"params":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"}]},"description":"With `message_id`: the message's parameters. Numbers and names from the email format or Flyanote's own lists, never text from the request.","example":{"max":100}}},"required":["in","pointer","detail"]},"Finding":{"type":"object","properties":{"check":{"type":"string","pattern":"^[a-z][a-z0-9]*(?:\\.[a-z_]+)+$"},"severity":{"type":"string","description":"What it holds back: `reject`, `block`, `bug`, `warning`, `info`, or `applied`. Treat a value you don’t know as one that blocks.","x-extensible-enum":["reject","block","bug","warning","info","applied"],"example":"block"},"stops_test_send":{"type":"boolean","enum":[true]},"code":{"type":"string"},"pointer":{"type":"string"},"block_id":{"type":"string"},"line":{"type":"integer","exclusiveMinimum":0},"column":{"type":"integer","exclusiveMinimum":0},"clients":{"type":"array","items":{"type":"string"}},"confidence":{"type":"string","enum":["calibrated","modeled","unknown"]},"message_key":{"type":"string"},"params":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"}]}},"fix":{"oneOf":[{"type":"object","properties":{"kind":{"type":"string","enum":["patch"]},"label_key":{"type":"string"},"patch":{"type":"array","items":{"type":"object","properties":{"op":{"type":"string","enum":["add","remove","replace","move","copy","test"]},"path":{"type":"string"},"from":{"type":"string"},"value":{}},"required":["op","path"],"additionalProperties":false}},"input":{"type":"string","enum":["text"]}},"required":["kind","label_key","patch"],"additionalProperties":false},{"type":"object","properties":{"kind":{"type":"string","enum":["operation"]},"label_key":{"type":"string"},"operation_id":{"type":"string"}},"required":["kind","label_key","operation_id"],"additionalProperties":false}]},"docs_url":{"type":"string","description":"Where `check` is explained: its entry in the checks reference, with how to fix it.","example":"https://flyanote.com/docs/checks#img-alt-quality"},"evidence":{"type":"object","additionalProperties":{}},"message":{"type":"string","description":"`message_key` in English, with `params` filled in. It never quotes the document, its data, or a contact. Branch on `check`, never on this text, which can change.","example":"This image has no alt text."}},"required":["check","severity","pointer","message_key","params","docs_url","message"],"additionalProperties":false,"description":"Something the health checks found in an email document. `check` is the catalog id (it never changes meaning), `severity` says what it holds back: `reject` (the document can't be stored), `block` (it can't be published or sent), `bug` (Flyanote's compiler broke an invariant), `warning`, `info`, or `applied` (something the compiler did for you). `pointer` is a JSON Pointer (RFC 6901) into the document, and `fix`, when there is one, is a JSON Patch to apply to the draft or an operation to run."},"Ping":{"type":"object","properties":{"note":{"type":"string"}},"required":["note"]},"OpenApiDocument":{"type":"object","properties":{"openapi":{"type":"string"},"info":{"type":"object","properties":{"title":{"type":"string"},"version":{"type":"string"}},"required":["title","version"]}},"required":["openapi","info"],"additionalProperties":{}},"AccountProfile":{"type":"object","properties":{"sends":{"type":["string","null"],"enum":["newsletters","app_mail","both",null],"description":"What the account sends: `newsletters` (newsletters and announcements), `app_mail` (receipts, password resets, and other app mail), or `both`. Null until someone says."},"industry":{"type":["string","null"],"enum":["faith","nonprofit","club","school","sports","arts","events","winery_brewery_distillery","restaurant","food_maker","local_shop","online_store","publishing","software","health_fitness","professional","other",null],"description":"The kind of organization: church or faith community (`faith`); charity or nonprofit (`nonprofit`); club, association, or membership group (`club`); school, camp, or youth program (`school`); sports team or league (`sports`); arts, music, or theater group (`arts`); events or venue (`events`); winery, brewery, or distillery (`winery_brewery_distillery`); restaurant, café, or bar (`restaurant`); farm, bakery, or food maker (`food_maker`); local shop or boutique (`local_shop`); online store (`online_store`); newsletter, blog, or podcast (`publishing`); software, app, or AI project (`software`); health, fitness, or wellness (`health_fitness`); coach, consultant, or other professional (`professional`); or `other`, in the account's own words (`industry_other`). Null until someone says."},"industry_other":{"type":["string","null"],"description":"The account's own words for `other`, or null."},"time_zone":{"type":"string","description":"An IANA time zone: the account's days for scheduling and reports. UTC until someone sets it.","example":"America/Halifax"},"contact_daily_limit":{"type":"boolean","description":"One email a day per person: at most one marketing email per person per day, the limit a campaign send will take by default. On by default. Not the account’s own daily sending limit, which isn’t a setting."},"set_at":{"type":["string","null"],"format":"date-time","description":"When the profile was first saved, by anyone; null until then. The app asks its welcome questions only while it’s null."}},"required":["sends","industry","industry_other","time_zone","contact_daily_limit","set_at"],"description":"What the account tells us about itself: what it sends and its kind of organization (which set up Home and, later, Reports’ benchmarks), its time zone, and whether its marketing mail is limited to one email a day per person."},"AccountProfileChange":{"type":"object","properties":{"sends":{"type":["string","null"],"enum":["newsletters","app_mail","both",null],"description":"What the account sends: `newsletters` (newsletters and announcements), `app_mail` (receipts, password resets, and other app mail), or `both`."},"industry":{"type":["string","null"],"enum":["faith","nonprofit","club","school","sports","arts","events","winery_brewery_distillery","restaurant","food_maker","local_shop","online_store","publishing","software","health_fitness","professional","other",null],"description":"The kind of organization: church or faith community (`faith`); charity or nonprofit (`nonprofit`); club, association, or membership group (`club`); school, camp, or youth program (`school`); sports team or league (`sports`); arts, music, or theater group (`arts`); events or venue (`events`); winery, brewery, or distillery (`winery_brewery_distillery`); restaurant, café, or bar (`restaurant`); farm, bakery, or food maker (`food_maker`); local shop or boutique (`local_shop`); online store (`online_store`); newsletter, blog, or podcast (`publishing`); software, app, or AI project (`software`); health, fitness, or wellness (`health_fitness`); coach, consultant, or other professional (`professional`); or `other`, in the account's own words (`industry_other`). Any value but `other` clears `industry_other`, and so does null."},"industry_other":{"type":["string","null"],"minLength":1,"maxLength":100,"description":"Only with `industry: other` in the same request."},"time_zone":{"type":"string","maxLength":64,"description":"An IANA time zone.","example":"America/Halifax"},"contact_daily_limit":{"type":"boolean"}},"additionalProperties":false,"description":"The fields to change. A field left out stays as it is; null clears `sends` or `industry`.","example":{"sends":"newsletters","industry":"faith","time_zone":"America/Halifax"}},"SenderProfile":{"type":"object","properties":{"version":{"type":"integer","minimum":1,"description":"Counts up from 1 with each change. Earlier versions are kept for 13 months after they were replaced, so a sent email can show what it said."},"legal_name":{"type":"string"},"postal_address":{"$ref":"#/components/schemas/PostalAddress"},"contact":{"$ref":"#/components/schemas/SenderContact"},"changed_at":{"type":"string","format":"date-time","description":"When the profile changed to this version."}},"required":["version","legal_name","postal_address","contact","changed_at"],"description":"Who sends the account’s mail: what every marketing footer says about the sender, as the law requires. Marketing email waits until it’s set."},"PostalAddress":{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":200,"description":"The street address, PO box, or private mailbox."},"line2":{"type":"string","minLength":1,"maxLength":200,"description":"A suite, unit, or building, if any."},"city":{"type":"string","minLength":1,"maxLength":100},"state":{"type":"string","minLength":1,"maxLength":100,"description":"The state, province, county, or region, where there is one."},"postal_code":{"type":"string","minLength":1,"maxLength":20},"country":{"type":"string","pattern":"^[A-Z]{2}$","description":"ISO 3166-1 alpha-2.","example":"CA"}},"required":["line1","city","country"],"additionalProperties":false,"description":"Where the sender receives mail. Every marketing footer shows it, as the law requires (CAN-SPAM, CASL).","example":{"line1":"12 Maple Street","city":"Halifax","state":"NS","postal_code":"B3H 1A1","country":"CA"}},"SenderContact":{"type":"object","properties":{"kind":{"type":"string","enum":["email","phone","url"]},"value":{"type":"string","minLength":1,"maxLength":2048}},"required":["kind","value"],"additionalProperties":false,"description":"One way to reach the sender, which the footer shows beside the address: an email address (`email`), a phone number that reaches a person or voicemail (`phone`), or an https web address (`url`).","example":{"kind":"email","value":"hello@example.com"}},"SenderProfileChange":{"type":"object","properties":{"legal_name":{"type":"string","minLength":1,"maxLength":200,"description":"The name the sender is legally known by, or trades under.","example":"Maple Street Bakery Ltd."},"postal_address":{"$ref":"#/components/schemas/PostalAddress"},"contact":{"$ref":"#/components/schemas/SenderContact"}},"required":["legal_name","postal_address","contact"],"additionalProperties":false},"ApiKey":{"type":"object","properties":{"id":{"type":"string","example":"key_01j9zq2k3m4n5p6q7r8s9t0v1w"},"name":{"type":"string"},"mode":{"type":"string","enum":["live","test"],"description":"`test` keys run everything but never send real mail."},"last4":{"type":"string","description":"The key's last four characters, to tell keys apart.","example":"Uh3s"},"scopes":{"type":"array","items":{"type":"string"},"example":["messages:send"]},"restrictions":{"type":"object","properties":{"domains":{"type":"array","items":{"type":"string"},"description":"The only sending domains the key may send from. Absent: any of the account's."}}},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":["string","null"],"format":"date-time","description":"When the key was last used, to within five minutes."},"expires_at":{"type":["string","null"],"format":"date-time","description":"When the key stops working. Null: it never expires."},"revoked_at":{"type":["string","null"],"format":"date-time","description":"When the key was revoked. A revocation is final."}},"required":["id","name","mode","last4","scopes","restrictions","created_at","last_used_at","expires_at","revoked_at"]},"CurrentApiKey":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"object","properties":{"account_id":{"type":"string","example":"acct_01j9zq2k3m4n5p6q7r8s9t0v1w"}},"required":["account_id"]}]},"Domain":{"type":"object","properties":{"id":{"type":"string","example":"dom_01j9zq2k3m4n5p6q7r8s9t0v1w"},"domain":{"type":"string","description":"As stored: lowercase, international names in punycode.","example":"example.com"},"mode":{"type":"string","description":"`own`: a domain you own. `flyanote_address`: a Flyanote address (`<name>.flyanote.email`)."},"setup":{"type":"object","properties":{"status":{"type":"string","description":"Flyanote's side: `pending` until your records point at Flyanote, `ready` once Flyanote has set the domain up behind them (within minutes of that), `failed` if it can't (see `failure`)."},"failure":{"type":["string","null"],"description":"Why setup failed, or null: `domain_in_use` (the domain, or a domain above it, is already verified in another Flyanote account, or held outside Flyanote), `identity_quota`, or `ses_refused`."}},"required":["status","failure"]},"verification":{"$ref":"#/components/schemas/DomainVerification"},"records":{"type":"array","items":{"$ref":"#/components/schemas/DnsRecord"},"description":"The exact records to add at your DNS host, each with what the last check found. Add them once: Flyanote can change keys and regions behind them without you touching DNS again."},"reply_to":{"$ref":"#/components/schemas/ReplyTo"},"created_at":{"type":"string","format":"date-time"}},"required":["id","domain","mode","setup","verification","records","reply_to","created_at"]},"DomainVerification":{"type":"object","properties":{"state":{"type":"string","description":"`checking` until it verifies (for up to 3 days); `verified` once it can send; `needs_attention` when a verified domain's records broke and sending from it stopped; `stopped` when 3 days passed without verifying, or 14 days in `needs_attention` released the domain (reasons say `released`), and checking stopped (check again to restart); `setup_failed` when Flyanote couldn't set it up (see `setup.failure`): check again does nothing, so remove it and add it again once the cause is fixed."},"dkim":{"type":"string","description":"`pending`, `verified`, or `failed`."},"mail_from":{"type":"string","description":"`pending`, `verified`, or `failed`."},"dmarc":{"type":"string","description":"`pending`, `verified` (a DMARC record that parses), or `failed`. DMARC never blocks sending."},"reasons":{"type":"object","properties":{"dkim":{"type":["string","null"]},"mail_from":{"type":["string","null"]},"dmarc":{"type":["string","null"]}},"required":["dkim","mail_from","dmarc"],"description":"Why each isn't verified, or null: `missing`, `wrong_value`, `duplicate`, `ses_failed`, `not_verified` (3 days passed), `refused` (sending was refused over it), `identity_missing`, `held` (another Flyanote account still holds the name, or a domain above it, unverified, until it gives it back), `released` (its records stayed broken for 14 days, so Flyanote gave the name up), or for DMARC `invalid`."},"checked_at":{"type":["string","null"],"format":"date-time"},"next_check_at":{"type":["string","null"],"format":"date-time","description":"Null once checking stopped."},"verified_at":{"type":["string","null"],"format":"date-time","description":"When DKIM and MAIL FROM first both verified."},"failed_at":{"type":["string","null"],"format":"date-time","description":"When the current failure began, or null."},"release_at":{"type":["string","null"],"format":"date-time","description":"While it needs attention and the account has been warned, when Flyanote releases the domain unless its records are back, as the warning emails say; otherwise null."},"check_again_at":{"type":["string","null"],"format":"date-time","description":"When the domain can next be checked on request (`POST …/{id}/check`), or null for now."}},"required":["state","dkim","mail_from","dmarc","reasons","checked_at","next_check_at","verified_at","failed_at","release_at","check_again_at"]},"DnsRecord":{"type":"object","properties":{"purpose":{"type":"string","description":"`dkim` signs your mail; `mail_from` aligns its bounces (SPF); `dmarc` (suggested, only when the domain has no DMARC record that works) tells inboxes you have DMARC."},"type":{"type":"string","example":"CNAME"},"name":{"type":"string","description":"The record name, in full.","example":"fan1._domainkey.example.com"},"host":{"type":"string","description":"The name without the domain, as most DNS hosts want it in their Host field (they add the domain themselves).","example":"fan1._domainkey"},"value":{"type":"string","example":"fan1.01j9zq2k3m4n5p6q7r8s9t0v1w.dkim.flyanote.email"},"required":{"type":"boolean","description":"DKIM and MAIL FROM are required to send; DMARC is recommended."},"status":{"type":"string","description":"`pending`, `verified`, or `failed`, as the last check found it."},"found":{"type":"boolean","description":"Whether DNS has this record exactly as shown, even if it isn't verified yet."},"problem":{"type":["string","null"],"description":"What's wrong with it in DNS, or null: `missing` (not there yet), `wrong_value` (there, but pointing somewhere else), `duplicate` (more than one), or for DMARC `invalid`."}},"required":["purpose","type","name","host","value","required","status","found","problem"]},"ReplyTo":{"type":["object","null"],"properties":{"mailbox":{"type":["string","null"],"description":"The mailbox replies go to, once its owner confirmed it, or null. The address sends only once it has one, and its mail always carries it as `Reply-To`.","example":"office@example.org"},"verified_at":{"type":["string","null"],"format":"date-time","description":"When its owner confirmed it."},"request":{"type":["object","null"],"properties":{"mailbox":{"type":"string","description":"The mailbox asked for.","example":"office@example.org"},"state":{"type":"string","description":"`pending` until its owner opens the email Flyanote sent it and confirms, for up to 3 days; `confirmed`; `cancelled` if they chose \"This wasn't me\"; or `expired` after 3 days without an answer (ask again)."}},"required":["mailbox","state"],"description":"The last confirmation asked for (`POST /v1/app/addresses/{id}/reply-to`), or null."}},"required":["mailbox","verified_at","request"],"description":"A Flyanote address's reply-to mailbox, where replies to its mail go. Null for a domain of your own."},"CreditBalance":{"type":"object","properties":{"available":{"$ref":"#/components/schemas/Money"},"available_decimal":{"type":"string","description":"`available` in the currency's major unit, exactly, for display.","example":"12.50"},"updated_at":{"type":["string","null"],"format":"date-time","description":"When the balance last moved."}},"required":["available","available_decimal","updated_at"]},"Money":{"type":"object","properties":{"amount_minor":{"type":"string","pattern":"^-?\\d+$","description":"A whole number of minor units (cents for USD), as a string.","example":"1250"},"currency":{"type":"string","description":"ISO 4217.","example":"USD"}},"required":["amount_minor","currency"],"description":"What the account can spend now: credit in, less usage, less what sends in flight hold."},"MessageAccepted":{"type":"object","properties":{"id":{"type":"string","example":"msg_01j9zq2k3m4n5p6q7r8s9t0v1w"},"status":{"type":"string","description":"`queued`: accepted, and sending shortly. `suppressed`: not sent, and not charged (see the message).","example":"queued"},"template":{"$ref":"#/components/schemas/SentTemplate"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/SendWarning"},"description":"What a template's render noticed. The message was accepted anyway. Always empty without a template."}},"required":["id","status","template","warnings"]},"SentTemplate":{"type":["object","null"],"properties":{"id":{"type":"string","example":"tpl_01j9zq2k3m4n5p6q7r8s9t0v1w"},"alias":{"type":"string","example":"receipt"},"version":{"type":"integer","description":"The published version sent.","example":3}},"required":["id","alias","version"],"description":"The template the message was rendered from, or null without one."},"SendWarning":{"type":"object","properties":{"code":{"type":"string","description":"`template_data_missing`: the template read fields `data` didn't have, which render empty there. A merge field with a fallback isn't counted where it's the only read, since its fallback shows. `link_dropped`: a link or image rendered with an address it can't have, so the address was taken out: a link's text stays, unlinked, and an image is left out. In the HTML, a link keeps an address that parses as an `http`, `https`, `mailto`, or `tel` URL (as a browser reads one: spaces and control characters at its ends, and tabs and newlines anywhere, ignored), an in-page one (`#`, then only ASCII letters, digits, `_`, `.`, `:`, or `-`), or one of nothing but spaces and control characters; an image keeps an `http` or `https` URL, or an empty one. Any other is taken out, a relative one included. In the text, a link is checked only where it reads `<address>` (see `positions`), and taken out unless it parses as an `http`, `https`, `mailto`, or `tel` URL. `message_size_over_clip`: the HTML is over 100 KB, so Gmail may cut it off. `subject_truncated`: the rendered subject was cut to 998 characters. More codes may come.","example":"template_data_missing"},"detail":{"type":"string","description":"What it means, in plain language."},"paths":{"type":"array","items":{"type":"string"},"description":"For `template_data_missing`: the fields, up to 100.","example":["data.order.number"]},"positions":{"type":"array","items":{"$ref":"#/components/schemas/DroppedUrl"},"description":"For `link_dropped`: where each one was, up to 100. Never its address, which came from `data`."}},"required":["code","detail"]},"DroppedUrl":{"type":"object","properties":{"part":{"type":"string","enum":["html","text"],"description":"The part it was in."},"kind":{"type":"string","enum":["link","image"]},"index":{"type":"integer","minimum":1,"description":"Its place among the part's links, or its images, counting from 1 in the order they appear, kept ones included. In the HTML, the links are its `href` attributes, and the images its `src` and `background` attributes and CSS `url()`s, Outlook's markup in comments included. In the text, where a link reads `text <address>`, the links are each `<`, then a scheme (after any spaces or control characters), then no space (U+0020) up to the next `>`.","example":3}},"required":["part","kind","index"]},"SendMessage":{"type":"object","properties":{"from":{"type":"object","properties":{"address":{"type":"string","maxLength":254,"description":"An address on one of the account's verified domains or its Flyanote address. With a `fan_test_` key, `test@sandbox.flyanote.com` also works before the account has a sender.","example":"receipts@example.com"},"name":{"type":"string","minLength":1,"maxLength":256,"description":"The display name recipients see.","example":"St. Mark's Parish"}},"required":["address"],"additionalProperties":false},"to":{"type":"string","maxLength":320,"description":"The one recipient. To send to several people, send one message each.","example":"jane@example.com"},"subject":{"type":"string","minLength":1,"maxLength":998,"description":"Required, unless the message is sent from a `template`, which has its own.","example":"Your receipt for order 1042"},"html":{"type":"string","minLength":1,"maxLength":262144,"description":"The HTML body, up to 256 KiB in UTF-8."},"text":{"type":"string","minLength":1,"maxLength":262144,"description":"The plain-text body, up to 256 KiB in UTF-8. Without it, Flyanote makes one from `html`."},"template":{"type":"string","minLength":1,"maxLength":64,"description":"A stored template to send, by its alias or its id (`tpl_…`), in place of `subject`, `html`, and `text`. Flyanote renders its newest published version, or `template_version`, with `data` before it answers, so a template that fails to render is refused here and nothing is sent. Only a `transactional` template can be sent here. With a test key, each one also counts toward the account’s `render-account` limit for test keys, as a render does.","example":"receipt"},"template_version":{"type":"integer","minimum":1,"description":"The number of the template's published version to send. Without it, the newest.","example":3},"data":{"type":"object","description":"The `data.*` the template reads: an object of at most 256 KiB of JSON, nested at most 10 levels, without NUL characters or unpaired surrogates. It reaches the message only where the template puts it, and no warning or error repeats it. A field the template reads that's missing here shows its fallback, and the answer's `warnings` names it.","example":{"order":{"number":"R-10442","total":5147,"currency":"USD"}}},"reply_to":{"type":"array","items":{"type":"string","maxLength":320},"minItems":1,"maxItems":10,"description":"Where replies go, if not the sender.","example":["office@example.com"]},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Extra headers, from this list only: `In-Reply-To`, `References`, `X-Entity-Ref-ID`. `In-Reply-To` and `References` take message ids in angle brackets, to thread a reply; `X-Entity-Ref-ID` stops Gmail from grouping unrelated messages.","example":{"X-Entity-Ref-ID":"order-1042"}},"tags":{"type":"object","additionalProperties":{"type":"string","maxLength":256},"description":"Your own labels, returned when you read the message: up to 10, each value up to 256 characters.","example":{"order":"1042"}},"category":{"type":"string","enum":["transactional"],"default":"transactional","description":"What the message is. Only `transactional` for now: receipts, resets, notifications."}},"required":["from","to"],"additionalProperties":false,"example":{"from":{"address":"receipts@example.com","name":"St. Mark's Parish"},"to":"jane@example.com","subject":"Your receipt for order 1042","html":"<p>Thanks, Jane! We received your gift of $25.</p>","tags":{"order":"1042"}}},"MessageWithEvents":{"allOf":[{"$ref":"#/components/schemas/Message"},{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/MessageEvent"},"description":"What happened to the message, oldest first."}},"required":["events"]}]},"MessageReason":{"type":["object","null"],"properties":{"code":{"type":"string","description":"A stable code to branch on.","example":"hard_bounce"},"detail":{"type":"string","description":"What it means, in plain language."},"retryable":{"type":"boolean","description":"True when sending the same message again, unchanged, may work once `retry_after` has passed. It isn’t a promise that it will. Read the message again first: if a late answer from the email service moved its status on, it was sent. Send it as a new request with a new `Idempotency-Key`, since the original key returns this message. False means don’t send it again unchanged: `detail` says what to do instead. `ses_outcome_unknown` is false because the message may have been sent, and so is a code this API version doesn’t know.","example":true},"retry_after":{"type":["integer","null"],"description":"Whole seconds, rounded up, left to wait before sending it again. The wait starts when the message failed, so reading it again doesn’t start it over: for a 15-minute wait, a read 10 minutes after the failure says 300. 0 means the wait is over. Null when `retryable` is false.","example":3600}},"required":["code","detail","retryable","retry_after"],"description":"Why a `failed` or `suppressed` message wasn’t sent. Null otherwise."},"MessageTemplate":{"type":["object","null"],"properties":{"id":{"type":"string","example":"tpl_01j9zq2k3m4n5p6q7r8s9t0v1w"},"alias":{"type":["string","null"],"example":"receipt"},"version":{"type":["integer","null"],"example":3}},"required":["id","alias","version"],"description":"The stored template the message was sent from, or null. Once the template is deleted, `alias` is null, and the id and `version` stay."},"MessageEvent":{"type":"object","properties":{"type":{"type":"string","description":"`send`, `delivery`, `deferral`, `bounce`, `complaint`, `not_spam`, `reject`, `failed`, or `suppressed`. `not_spam` is a feedback report that the recipient moved the mail out of spam (code `fbl_not_spam`): it isn’t a complaint, and never marks the message `complained`.","example":"delivery"},"at":{"type":"string","format":"date-time"},"code":{"type":["string","null"],"description":"A code for what happened, if any.","example":"user_unknown"},"bounce_class":{"type":["string","null"],"description":"For a bounce: its type and subtype.","example":"permanent_no_email"}},"required":["type","at","code","bounce_class"]},"Message":{"type":"object","properties":{"id":{"type":"string","example":"msg_01j9zq2k3m4n5p6q7r8s9t0v1w"},"status":{"type":"string","description":"`queued` → `sending` → `sent` → `delivered`, or `bounced` or `complained` after that. `failed` and `suppressed` were never sent (see `reason`). A test key's messages end as `test`.","example":"delivered"},"mode":{"type":"string","description":"`live`, or `test` for a test key’s message, which never delivers."},"category":{"type":"string","example":"transactional"},"from":{"type":"object","properties":{"address":{"type":"string"},"name":{"type":["string","null"]}},"required":["address","name"]},"to":{"type":"string","example":"jane@example.com"},"reply_to":{"type":"array","items":{"type":"string"}},"subject":{"type":"string"},"tags":{"type":"object","additionalProperties":{"type":"string"}},"created_at":{"type":"string","format":"date-time"},"status_changed_at":{"type":"string","format":"date-time"},"reason":{"$ref":"#/components/schemas/MessageReason"},"template":{"$ref":"#/components/schemas/MessageTemplate"}},"required":["id","status","mode","category","from","to","reply_to","subject","tags","created_at","status_changed_at","reason","template"]}}}}