{
  "openapi": "3.1.0",
  "info": {
    "title": "SendForAI API",
    "version": "1",
    "summary": "Transactional and broadcast email for AI agents, at cost.",
    "description": "One API for one-to-one transactional sends and broadcasts to a list, on Amazon SES with zero markup on sending.\n\nThere are no API keys. Generate an Ed25519 keypair, publish the public key as DNS TXT at `_sendforai.<domain>` = `v=sfa1; k=ed25519; p=<base64>` (GET /v1/domains/{domain}/setup?p=<key> returns the exact record), then sign requests or mint a connection token. The first verified request creates the account. No domain yet: use a sandbox identity (name derived from the key, no DNS), which can use the whole API against the simulator.\n\nTypical flow: POST /v1/domains/{domain}/verify and publish the returned DKIM and MAIL FROM records; POST /v1/emails with dry_run: true; then send for real with idempotency_key and max_cost_usd. Test with any address @simulator.sendforai.com (free, never delivered).\n\nAll bodies are JSON. Money is in USD. Every error has the same shape and carries error.fix: read it and do what it says. Paying and raising the budget are the only steps that need a human; the API returns a link to hand to your operator.",
    "contact": {
      "name": "SendForAI",
      "email": "hello@sendforai.com",
      "url": "https://sendforai.com"
    }
  },
  "externalDocs": {
    "description": "Full documentation (Markdown)",
    "url": "https://sendforai.com/docs.md"
  },
  "servers": [
    {
      "url": "https://api.sendforai.com"
    }
  ],
  "security": [
    {
      "connectionToken": []
    },
    {
      "signatureDomain": [],
      "signatureTimestamp": [],
      "signature": []
    }
  ],
  "tags": [
    {
      "name": "Emails",
      "description": "Send transactional email."
    },
    {
      "name": "Account",
      "description": "Balance, budget, credit, capacity and usage."
    },
    {
      "name": "Domains",
      "description": "Identity and sending domains, DNS records, SES verification."
    },
    {
      "name": "Broadcasts",
      "description": "Send to a list of contacts, with human approval by default."
    },
    {
      "name": "Contacts",
      "description": "The list broadcasts send to."
    },
    {
      "name": "Events",
      "description": "Pullable event log; no webhooks required."
    },
    {
      "name": "Meta",
      "description": "Public discovery, pricing and health."
    }
  ],
  "paths": {
    "/v1": {
      "get": {
        "operationId": "getIndex",
        "summary": "API index",
        "tags": [
          "Meta"
        ],
        "description": "Public. Where to find docs, pricing, status, the MCP server, CLI and SDK. Start here.",
        "security": [],
        "responses": {
          "200": {
            "description": "Index of entry points.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          }
        }
      }
    },
    "/v1/pricing": {
      "get": {
        "operationId": "getPricing",
        "summary": "Machine-readable pricing",
        "tags": [
          "Meta"
        ],
        "description": "Public. Use it to compute cost before committing. Cacheable for an hour.",
        "security": [],
        "responses": {
          "200": {
            "description": "Current pricing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pricing"
                }
              }
            }
          }
        }
      }
    },
    "/v1/status": {
      "get": {
        "operationId": "getStatus",
        "summary": "Health check",
        "tags": [
          "Meta"
        ],
        "description": "Public. Reports whether the API is up and can reach its database.",
        "security": [],
        "responses": {
          "200": {
            "description": "Healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceStatus"
                }
              }
            }
          },
          "503": {
            "description": "Degraded: the database is unreachable. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceStatus"
                }
              }
            }
          }
        }
      }
    },
    "/v1/emails": {
      "post": {
        "operationId": "sendEmail",
        "summary": "Send an email",
        "tags": [
          "Emails"
        ],
        "description": "Send one email to 1-50 recipients (each gets its own message). Provide html, text or markdown.\n\nDo this first: send with dry_run: true to see cost, capacity and lint without sending; then send for real with idempotency_key (safe to retry) and max_cost_usd (hard cost ceiling).\n\nTesting: addresses @simulator.sendforai.com are free, uncapped and never delivered. delivered@, bounced@ and complained@ simulate those outcomes (any other local part is delivered); the events appear in GET /v1/events. Do not mix simulator and real addresses in one request. Sandbox identities can only send to the simulator.\n\nBilling: sends to addresses at your own domains are free (25 a day). The first send in a UTC month to anyone else takes the $5 sending-month fee from the prepaid balance (10,000 emails included), then $0.11 per 1,000. A dry run reports this as cost.month_fee_usd.\n\nReturns 201 when sent; 200 for a dry run (status planned), an idempotent replay (header Idempotent-Replayed: true), or when every recipient is suppressed (status suppressed).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendEmailRequest"
              },
              "example": {
                "from": "Acme <hello@example.com>",
                "to": "ada@customer.org",
                "subject": "Your receipt",
                "markdown": "# Thanks for your order\n\nYour receipt is attached to your account.",
                "idempotency_key": "order-1042-receipt",
                "dry_run": true,
                "max_cost_usd": 5
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Nothing new was sent: dry run (status planned), idempotent replay (status sent, header Idempotent-Replayed: true), or all recipients suppressed (status suppressed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendEmailResponse"
                },
                "example": {
                  "id": null,
                  "dry_run": true,
                  "status": "planned",
                  "ses_message_id": null,
                  "recipients": [
                    {
                      "to": "ada@customer.org",
                      "status": "accepted"
                    }
                  ],
                  "cost": {
                    "emails": 1,
                    "ses_usd": 0.0001,
                    "billed_usd": 0,
                    "included_remaining": 9999,
                    "month_fee_usd": 5,
                    "note": "Starts this month's sending period: $5 from your balance, 10,000 emails included. Included in this sending month. 9999 of 10000 included emails remain this month."
                  },
                  "capacity": {
                    "domain": "example.com",
                    "plan": "member",
                    "status": "verified",
                    "warmup_stage": 0,
                    "daily_cap": 1000,
                    "sent_today": 0,
                    "remaining_today": 1000,
                    "monthly_cap": null,
                    "sent_this_month": 0,
                    "remaining_this_month": null,
                    "resets_at": "2026-11-13T00:00:00.000Z"
                  },
                  "lint": [],
                  "receipt": null
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "description": "\"true\" when this is a replay of an earlier request with the same idempotency_key.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "201": {
            "description": "Sent. Check recipients for any per-address failure. Track delivery with GET /v1/events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendEmailResponse"
                },
                "example": {
                  "id": "7c2f1f0e-5b1a-4a6e-9d3c-2f8e4b6a1c90",
                  "dry_run": false,
                  "status": "sent",
                  "ses_message_id": "0100019328a1b2c3-4d5e6f70-0000-0000-0000-000000000000-000000",
                  "recipients": [
                    {
                      "to": "ada@customer.org",
                      "status": "accepted"
                    }
                  ],
                  "cost": {
                    "emails": 1,
                    "ses_usd": 0.0001,
                    "billed_usd": 0,
                    "included_remaining": 9999,
                    "note": "Included in this sending month. 9999 of 10000 included emails remain this month."
                  },
                  "capacity": {
                    "domain": "example.com",
                    "plan": "member",
                    "status": "verified",
                    "warmup_stage": 0,
                    "daily_cap": 1000,
                    "sent_today": 1,
                    "remaining_today": 999,
                    "monthly_cap": null,
                    "sent_this_month": 1,
                    "remaining_this_month": null,
                    "resets_at": "2026-11-13T00:00:00.000Z"
                  },
                  "lint": [],
                  "receipt": "eyJpZCI6IjdjMmYxZjBlLTViMWEtNGE2ZS05ZDNjLTJmOGU0YjZhMWM5MCIsImRvbWFpbiI6ImV4YW1wbGUuY29tIiwicmVjaXBpZW50cyI6MX0.qv3Zr0m1b8Jk2uT5yN7xLw4cHd9aEoPfGs6iKjUlVtA"
                }
              }
            },
            "headers": {
              "SendForAI-Simulated": {
                "description": "\"true\" when every recipient was a simulator address.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "400 bad_request. Validation failed (details.issues), simulator and real addresses were mixed, or from is not on a domain this account can send from. For the last case details.record is the delegation TXT record to publish; then POST /v1/domains.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "409": {
            "description": "409. sandbox_identity: a sandbox identity can only send to @simulator.sendforai.com; add a domain you control (POST /v1/domains). domain_not_verified: the sending domain's SES identity is not verified; publish the DKIM and MAIL FROM records in details.records, then POST /v1/domains/{domain}/verify.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "domain_not_verified",
                    "message": "example.com is identified but its SES identity is not verified yet, so it cannot send to other people.",
                    "fix": "Publish the DKIM and MAIL FROM records returned in details.records, then call POST /v1/domains/example.com/verify again. Until then, test with simulated sends to any address @simulator.sendforai.com.",
                    "docs": "https://sendforai.com/docs/domains",
                    "details": {
                      "records": {
                        "dkim": [
                          {
                            "name": "abc123._domainkey.example.com",
                            "type": "CNAME",
                            "value": "abc123.dkim.amazonses.com"
                          }
                        ],
                        "mail_from": [
                          {
                            "name": "mail.example.com",
                            "type": "MX",
                            "value": "10 feedback-smtp.us-east-1.amazonses.com"
                          },
                          {
                            "name": "mail.example.com",
                            "type": "TXT",
                            "value": "v=spf1 include:amazonses.com ~all"
                          }
                        ],
                        "ses_status": "pending"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "422 send_rejected. Amazon SES refused every recipient. Nothing was sent and nothing was billed. details.recipients has the reason per address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/CapacityExceeded"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/account": {
      "get": {
        "operationId": "getAccount",
        "summary": "Get account",
        "tags": [
          "Account"
        ],
        "description": "Plan this month, balance, auto top-up, budget (monthly, used, remaining), usage and every sending domain. Check this before large sends.",
        "responses": {
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadAuthHeader"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/account/topup": {
      "post": {
        "operationId": "createTopup",
        "summary": "Add credit",
        "tags": [
          "Account"
        ],
        "description": "Returns a Stripe Checkout link for a human to pay. amount_usd defaults to 10 (minimum 10, maximum 5000). With auto_topup: true the card is saved and charged automatically when a send would not fit the balance. The body may be empty.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TopupRequest"
              },
              "example": {
                "amount_usd": 10,
                "auto_topup": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Checkout link created. Hand checkout_url to your operator; account.topped_up appears in GET /v1/events when they pay.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopupResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/BillingUnavailable"
          }
        }
      }
    },
    "/v1/account/upgrade": {
      "post": {
        "operationId": "upgradeAccount",
        "summary": "Add credit (alias of topup)",
        "tags": [
          "Account"
        ],
        "description": "Alias of POST /v1/account/topup, kept for older clients. There is no plan to buy: going live means having credit. Prefer /v1/account/topup.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TopupRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Checkout link created. Hand checkout_url to your operator; account.topped_up appears in GET /v1/events when they pay.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopupResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/BillingUnavailable"
          }
        }
      }
    },
    "/v1/account/budget": {
      "post": {
        "operationId": "setBudget",
        "summary": "Set the monthly usage budget",
        "tags": [
          "Account"
        ],
        "description": "Lowering (or keeping) the budget applies immediately (200). Raising it returns 202 with approval_url for a human; the budget is unchanged until they approve, then account.budget_changed appears in GET /v1/events.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BudgetRequest"
              },
              "example": {
                "monthly_usd": 50
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Budget lowered. Applied now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetResponse"
                },
                "example": {
                  "status": "updated",
                  "monthly_usd": 10
                }
              }
            }
          },
          "202": {
            "description": "Raise requested. Hand approval_url to your operator.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/account/auto-topup": {
      "post": {
        "operationId": "setAutoTopup",
        "summary": "Turn auto top-up on or off",
        "tags": [
          "Account"
        ],
        "description": "Turn auto top-up off, or change its amount. Turning it on needs a saved card, which only comes from a human completing a top-up (POST /v1/account/topup with auto_topup: true).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutoTopupRequest"
              },
              "example": {
                "enabled": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "409 no_card_on_file. No saved card. Ask your operator to complete POST /v1/account/topup {\"auto_topup\": true}.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/account/billing": {
      "get": {
        "operationId": "getBilling",
        "summary": "Get a billing portal link",
        "tags": [
          "Account"
        ],
        "description": "Returns a short-lived Stripe billing portal link for a human (update the saved card, download receipts). Hand billing_url to your operator.",
        "responses": {
          "200": {
            "description": "Portal link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadAuthHeader"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "409 no_billing_account. No billing relationship yet. Add credit first with POST /v1/account/topup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/BillingUnavailable"
          }
        }
      }
    },
    "/v1/domains": {
      "get": {
        "operationId": "listDomains",
        "summary": "List sending domains",
        "tags": [
          "Domains"
        ],
        "description": "Every sending domain on this account, with status, warmup stage and daily cap.",
        "responses": {
          "200": {
            "description": "Domains and the per-account limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadAuthHeader"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      },
      "post": {
        "operationId": "addDomain",
        "summary": "Add a sending domain",
        "tags": [
          "Domains"
        ],
        "description": "Add another domain to send from with the same key. First publish TXT _sendforai.<name> = \"v=sfa1; delegate=<identity domain>\". Returns 202 awaiting_dns with the record if it is not visible yet (publish it, then call again); 201 added with the SES DKIM and MAIL FROM records to publish. Subdomains of a domain you already have need no call. Safe to repeat.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddDomainRequest"
              },
              "example": {
                "name": "news.example.org"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added. Publish the records in ses, then GET /v1/domains/{domain} until ses.status is verified.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddDomainResponse"
                }
              }
            }
          },
          "202": {
            "description": "Delegation record not found in DNS yet. Publish record, then call this again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddDomainResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "409": {
            "description": "409. already_covered: it is your identity domain or a subdomain; just send from it. domain_taken: the domain belongs to another account. domain_limit: the account is at its domain limit; ask your operator to email hello@sendforai.com.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/domains/{domain}": {
      "get": {
        "operationId": "getDomain",
        "summary": "Get domain status",
        "tags": [
          "Domains"
        ],
        "description": "Identity record, SES verification state, the DKIM and MAIL FROM records to publish, and capacity for one of the account's domains. next says what to do.",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A domain name, e.g. example.com."
          }
        ],
        "responses": {
          "200": {
            "description": "Domain status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainStatus"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadAuthHeader"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/domains/{domain}/setup": {
      "get": {
        "operationId": "getDomainSetup",
        "summary": "Get the identity DNS record",
        "tags": [
          "Domains"
        ],
        "description": "Public. Given your Ed25519 public key, returns the one TXT record that turns a domain into a SendForAI account. Publish it, then sign requests with the matching private key; the first verified request creates the account.",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A domain name, e.g. example.com."
          },
          {
            "name": "p",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Raw 32-byte Ed25519 public key, base64 (URL-encode it)."
          }
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The record to publish.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainSetup"
                },
                "example": {
                  "domain": "example.com",
                  "identity_record": {
                    "name": "_sendforai.example.com",
                    "type": "TXT",
                    "value": "v=sfa1; k=ed25519; p=3XJ0m1m3m6k0b1o0Yk3q8J8m2yq3d2bq1v7q1Jk0x9Q="
                  },
                  "next": "Publish that TXT record. Once it resolves, sign requests with the matching private key and your account exists. Then POST /v1/domains/example.com/verify to get the DKIM records SES needs before you can send."
                }
              }
            }
          },
          "400": {
            "description": "400 bad_request. p is missing or is not a base64 raw 32-byte Ed25519 public key (not a PEM).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/domains/{domain}/verify": {
      "post": {
        "operationId": "verifyDomain",
        "summary": "Create or refresh the SES identity",
        "tags": [
          "Domains"
        ],
        "description": "Creates (or refreshes) the Amazon SES identity for one of the account's domains and returns the DKIM and MAIL FROM records to publish. No request body. Idempotent: call again after publishing; ses.status becomes verified once DNS propagates.",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A domain name, e.g. example.com."
          }
        ],
        "responses": {
          "200": {
            "description": "Domain status with records to publish.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainStatus"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadAuthHeader"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "409 sandbox_identity. A sandbox identity has no DNS to verify. Add a domain you control with POST /v1/domains, then verify that.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/capacity": {
      "get": {
        "operationId": "getCapacity",
        "summary": "Get sending capacity",
        "tags": [
          "Account"
        ],
        "description": "Daily cap, sent today, remaining and warmup stage for the identity domain. Check before a batch.",
        "responses": {
          "200": {
            "description": "Capacity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Capacity"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadAuthHeader"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Get month-to-date usage",
        "tags": [
          "Account"
        ],
        "description": "Emails sent, SES cost, billed cost and a straight-line projection for the current UTC month.",
        "responses": {
          "200": {
            "description": "Usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadAuthHeader"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/broadcasts": {
      "post": {
        "operationId": "createBroadcast",
        "summary": "Create a broadcast",
        "tags": [
          "Broadcasts"
        ],
        "description": "Create a send to all subscribed, unsuppressed contacts (optionally filtered by contact data). By default it is created awaiting_approval and returns approval_url: hand that to a human; approving starts the send. With require_approval: false it is created approved; then call POST /v1/broadcasts/{id}/send. dry_run returns the estimate (200) without creating anything. Add contacts first with POST /v1/contacts.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateBroadcastRequest"
              },
              "example": {
                "from": "Acme <news@example.com>",
                "subject": "November update",
                "markdown": "# What's new\n\nThree things shipped this month.",
                "filter": {
                  "plan": "pro"
                },
                "max_cost_usd": 10
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dry run: estimate only, id is \"dry_run\", status draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Broadcast"
                }
              }
            }
          },
          "201": {
            "description": "Created. If awaiting_approval, hand approval_url to a human; otherwise call send.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Broadcast"
                }
              }
            }
          },
          "400": {
            "description": "400 bad_request. Validation failed, from is not on a domain this account can send from, or no subscribed, unsuppressed contacts match the filter (add contacts or loosen the filter).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "402 budget_exceeded. The estimated billed cost is above max_cost_usd. Raise max_cost_usd or narrow the filter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/broadcasts/{id}": {
      "get": {
        "operationId": "getBroadcast",
        "summary": "Get a broadcast",
        "tags": [
          "Broadcasts"
        ],
        "description": "Status, cost and, once sending starts, progress (sent, queued, failed, cancelled, resumes_at). Poll this to follow a send.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Broadcast id."
          }
        ],
        "responses": {
          "200": {
            "description": "The broadcast.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Broadcast"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadAuthHeader"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/broadcasts/{id}/send": {
      "post": {
        "operationId": "sendBroadcast",
        "summary": "Send an approved broadcast",
        "tags": [
          "Broadcasts"
        ],
        "description": "Start sending an approved broadcast. No request body. Returns 202 while it sends in the background (large lists respect the daily cap and may resume the next day); poll GET /v1/broadcasts/{id}. Calling it again on a sending or sent broadcast is a no-op. The estimated cost is reserved now and the unsent remainder is refunded when it settles.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Broadcast id."
          }
        ],
        "responses": {
          "200": {
            "description": "Already sent. Nothing to do.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Broadcast"
                }
              }
            }
          },
          "202": {
            "description": "Sending in the background. Poll GET /v1/broadcasts/{id} for progress.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Broadcast"
                }
              }
            }
          },
          "400": {
            "description": "400 bad_request. No subscribed, unsuppressed contacts match this broadcast any more, or SendForAI-Domain is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "409. approval_required: a human must approve first; hand details.approval_url to a person. broadcast_closed: it was rejected or cancelled; create a new broadcast.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/CapacityExceeded"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/broadcasts/{id}/cancel": {
      "post": {
        "operationId": "cancelBroadcast",
        "summary": "Cancel a broadcast",
        "tags": [
          "Broadcasts"
        ],
        "description": "Cancel a draft, awaiting_approval, approved or sending broadcast. No request body. A sending broadcast stops after the batch in flight; emails already sent stay sent and the unsent part is refunded.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Broadcast id."
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Broadcast"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadAuthHeader"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "409 broadcast_not_cancellable. It is already sent, rejected or cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/contacts": {
      "post": {
        "operationId": "upsertContact",
        "summary": "Create or update a contact",
        "tags": [
          "Contacts"
        ],
        "description": "Upsert one contact by email. data replaces the stored data when given; subscribed is left unchanged when omitted on an existing contact. Available on every plan.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertContactRequest"
              },
              "example": {
                "email": "ada@customer.org",
                "data": {
                  "plan": "pro",
                  "first_name": "Ada"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The contact (created or updated).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      },
      "get": {
        "operationId": "listContacts",
        "summary": "List contacts",
        "tags": [
          "Contacts"
        ],
        "description": "Contacts ordered by id. Pass next_cursor as cursor until it is null.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            },
            "description": "Page size. Values outside 1-1000 are clamped."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "next_cursor from the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of contacts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadAuthHeader"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/contacts/import": {
      "post": {
        "operationId": "importContacts",
        "summary": "Bulk import contacts",
        "tags": [
          "Contacts"
        ],
        "description": "Upsert 1-10,000 contacts in one call. Only available in a sending month (plan member). Existing contacts are kept as they are (their data and subscribed are not overwritten). Duplicates within the request are collapsed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportContactsRequest"
              },
              "example": {
                "contacts": [
                  {
                    "email": "ada@customer.org",
                    "data": {
                      "plan": "pro"
                    }
                  },
                  {
                    "email": "grace@customer.org"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Import result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportContactsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "402 upgrade_required. Bulk import opens once this month's sending period has started. Until then add contacts one at a time with POST /v1/contacts; add credit with POST /v1/account/topup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/contacts/unsubscribe": {
      "post": {
        "operationId": "unsubscribeContact",
        "summary": "Unsubscribe a contact",
        "tags": [
          "Contacts"
        ],
        "description": "Mark a contact unsubscribed and add the address to the suppression list, so no later send reaches it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UnsubscribeContactRequest"
              },
              "example": {
                "email": "ada@customer.org"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The contact, now unsubscribed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "404 not_found. No contact with that email. Check the address, or create it with subscribed: false.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/events": {
      "get": {
        "operationId": "listEvents",
        "summary": "Pull the event log",
        "tags": [
          "Events"
        ],
        "description": "Events oldest first: email.* (sent, delivered, bounced, complained, failed), broadcast.*, account.* (month_started, topped_up, budget_changed, auto_topup_failed), domain.added, contact.unsubscribed. No webhook needed: keep the last non-null next_cursor and poll with it.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            },
            "description": "Page size. Values outside 1-1000 are clamped."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "next_cursor from a previous response, unchanged (URL-encode it)."
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only events of exactly this type, e.g. email.bounced."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventList"
                }
              }
            }
          },
          "400": {
            "description": "400 bad_request. cursor is malformed (pass next_cursor from a previous response unchanged), or SendForAI-Domain is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "connectionToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sfat1",
        "description": "A connection token minted locally with the identity key: `sfa.tokens.create()` in @sendforai/sdk or `npx sendforai token`. Send it as `Authorization: Bearer sfat1.<payload>.<signature>`. Nothing is stored server-side; a token lives at most 90 days (30 by default) and stops working if the key is rotated. On 401 with an expired token, mint a new one. Use this when you cannot sign each request (curl, hosted MCP, no-code tools)."
      },
      "signatureDomain": {
        "type": "apiKey",
        "in": "header",
        "name": "SendForAI-Domain",
        "description": "Part of the per-request signature (use together with SendForAI-Timestamp and SendForAI-Signature). The identity domain signing the request, bare, e.g. example.com."
      },
      "signatureTimestamp": {
        "type": "apiKey",
        "in": "header",
        "name": "SendForAI-Timestamp",
        "description": "Part of the per-request signature. Current Unix time in seconds. Rejected if more than 300 seconds from server time."
      },
      "signature": {
        "type": "apiKey",
        "in": "header",
        "name": "SendForAI-Signature",
        "description": "Sign every request with the identity's Ed25519 private key. Send three headers together: SendForAI-Domain, SendForAI-Timestamp, SendForAI-Signature.\n\nCanonical string: these six values joined by a single newline (\\n), no trailing newline:\n\n1. `sfa1`\n2. the HTTP method, uppercase (e.g. `POST`)\n3. the path with query string exactly as sent (e.g. `/v1/events?limit=10`)\n4. the SendForAI-Timestamp value (Unix seconds)\n5. the SendForAI-Domain value, trimmed, lowercased, without a trailing dot\n6. lowercase hex SHA-256 of the exact request body bytes (of the empty string for GET or no body)\n\ni.e. `sfa1\\nMETHOD\\n/path?query\\ntimestamp\\ndomain\\nsha256hex(body)`. Sign its UTF-8 bytes with Ed25519 and send `SendForAI-Signature: sfa1=<base64 signature>` (standard base64).\n\nThe public key is read from DNS TXT `_sendforai.<domain>` = `v=sfa1; k=ed25519; p=<base64 raw 32-byte public key>`. Sandbox identities (names ending .sandbox.sendforai.com, i.e. `k` + first 24 hex chars of sha256(raw public key) + `.sandbox.sendforai.com`) have no DNS: also send the raw base64 public key in a `SendForAI-Public-Key` header (required on the first request, harmless after). @sendforai/sdk and `npx sendforai` do all of this for you."
      }
    },
    "responses": {
      "Unauthenticated": {
        "description": "401 unauthenticated. Missing, malformed, expired or non-verifying credentials, or no identity record in DNS. error.fix says exactly what to publish or re-sign.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "unauthenticated",
                "message": "No identity record found at _sendforai.example.com.",
                "fix": "Publish a DNS TXT record at _sendforai.example.com with value \"v=sfa1; k=ed25519; p=<your base64 Ed25519 public key>\". GET /v1/domains/example.com/setup?p=<public key> returns the exact record. DNS can take a few minutes to propagate. Then retry this request.",
                "docs": "https://sendforai.com/docs.md#signing",
                "details": {
                  "record_name": "_sendforai.example.com",
                  "setup_url": "https://api.sendforai.com/v1/domains/example.com/setup"
                }
              }
            }
          }
        }
      },
      "BadRequest": {
        "description": "400 bad_request. Invalid JSON, a field failed validation (details.issues lists path and message for each), or SendForAI-Domain is not a valid domain name. Fix the listed fields and retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "bad_request",
                "message": "Invalid request: subject: Invalid input: expected string, received undefined",
                "fix": "Fix the listed fields and retry. See details.issues for each one.",
                "details": {
                  "issues": [
                    {
                      "path": "subject",
                      "message": "Invalid input: expected string, received undefined"
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "BadAuthHeader": {
        "description": "400 bad_request. SendForAI-Domain is not a valid domain name. Use a bare domain like example.com.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "402. balance_required: not enough prepaid credit for this send or to start a sending month; call POST /v1/account/topup and hand checkout_url to your operator. budget_exceeded: this month's usage budget would be exceeded, or the cost is above your max_cost_usd; send less, raise max_cost_usd, or POST /v1/account/budget for an approval link.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "balance_required": {
                "summary": "Not enough credit",
                "value": {
                  "error": {
                    "code": "balance_required",
                    "message": "Starting a sending month ($5) needs $5.00 more than this account's balance of $0.00.",
                    "fix": "Ask your operator to add credit: POST /v1/account/topup {\"amount_usd\": 10, \"auto_topup\": true} returns a checkout link. Until then, sends to @simulator.sendforai.com and to your own domains keep working on the free build tier.",
                    "details": {
                      "balance_usd": 0,
                      "shortfall_usd": 5,
                      "suggested_topup_usd": 10,
                      "topup": "/v1/account/topup"
                    }
                  }
                }
              },
              "budget_exceeded": {
                "summary": "Monthly budget would be exceeded",
                "value": {
                  "error": {
                    "code": "budget_exceeded",
                    "message": "This send would bring this month's usage to $20.0110, above the monthly budget of $20.00.",
                    "fix": "Send less, wait for next month, or ask your operator to raise the budget: POST /v1/account/budget {\"monthly_usd\": <new amount>} returns an approval link for them.",
                    "details": {
                      "used_usd": 19.9,
                      "budget_usd": 20,
                      "this_send_usd": 0.111
                    }
                  }
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "404 not_found. The resource does not exist on this account. error.fix says how to create or find it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CapacityExceeded": {
        "description": "429 capacity_exceeded. The send does not fit today's cap or the free monthly allowance, or the domain is paused. details has requested, remaining_today, daily_cap and resets_at; send fewer now or wait until resets_at.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "capacity_exceeded",
                "message": "This would send 40 emails but only 25 remain in today's cap of 25 for example.com.",
                "fix": "The free build tier allows 25 real emails a day to your own domains (simulated sends to @simulator.sendforai.com are unlimited). Send fewer now or wait until 2026-11-13T00:00:00.000Z.",
                "details": {
                  "requested": 40,
                  "remaining_today": 25,
                  "daily_cap": 25,
                  "resets_at": "2026-11-13T00:00:00.000Z",
                  "plan": "free"
                }
              }
            }
          }
        }
      },
      "BillingUnavailable": {
        "description": "503 billing_unavailable. Billing is not configured on this deployment. Ask your operator to contact hello@sendforai.com.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Internal": {
        "description": "500 internal. Something failed on our side. Retry with the same idempotency_key; if it keeps failing, GET /v1/status.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code. Known values: unauthenticated, bad_request, balance_required, budget_exceeded, upgrade_required, not_found, sandbox_identity, domain_not_verified, already_covered, domain_taken, domain_limit, no_card_on_file, no_billing_account, approval_required, broadcast_closed, broadcast_not_cancellable, send_rejected, capacity_exceeded, billing_unavailable, internal."
              },
              "message": {
                "type": "string",
                "description": "What happened."
              },
              "fix": {
                "type": "string",
                "description": "What to do next. Written for an agent to act on without a human."
              },
              "docs": {
                "type": "string",
                "format": "uri",
                "description": "Link to the relevant documentation."
              },
              "details": {
                "type": "object",
                "additionalProperties": true,
                "description": "Structured data for the fix, e.g. DNS records to publish, amounts, per-recipient reasons."
              }
            },
            "required": [
              "code",
              "message",
              "fix"
            ]
          }
        },
        "required": [
          "error"
        ],
        "description": "Every error has this shape. Read error.fix and do what it says."
      },
      "Plan": {
        "type": "string",
        "enum": [
          "free",
          "member"
        ],
        "description": "\"member\" in a calendar month (UTC) where the $5 sending-month fee has been paid; otherwise \"free\" (build tier)."
      },
      "Stream": {
        "type": "string",
        "enum": [
          "transactional",
          "broadcast"
        ]
      },
      "Cost": {
        "type": "object",
        "properties": {
          "emails": {
            "type": "integer",
            "description": "Emails this cost covers."
          },
          "ses_usd": {
            "type": "number",
            "description": "What Amazon SES charges SendForAI for these emails, in USD."
          },
          "billed_usd": {
            "type": "number",
            "description": "What you are billed, in USD. 0 while inside the month's included emails."
          },
          "included_remaining": {
            "type": "integer",
            "description": "Included emails left this month after this send. -1 when not applicable (simulated sends, some idempotent replays)."
          },
          "note": {
            "type": "string",
            "description": "Plain-language explanation of the charge."
          },
          "month_fee_usd": {
            "type": "number",
            "description": "Only on a dry run (or broadcast estimate) that would start a sending month: the monthly fee that will be taken from the balance first."
          }
        },
        "required": [
          "emails",
          "ses_usd",
          "billed_usd",
          "included_remaining",
          "note"
        ]
      },
      "Capacity": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "description": "Sending domain these numbers apply to."
          },
          "plan": {
            "$ref": "#/components/schemas/Plan"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "verified",
              "paused"
            ],
            "description": "\"paused\" means sending is blocked for bounce or complaint rate."
          },
          "warmup_stage": {
            "type": "integer",
            "description": "0-5. The daily cap rises with the stage."
          },
          "daily_cap": {
            "type": "integer",
            "description": "Real emails allowed per UTC day. -1 means no cap."
          },
          "sent_today": {
            "type": "integer"
          },
          "remaining_today": {
            "type": "integer",
            "description": "-1 means no cap."
          },
          "monthly_cap": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Free build tier monthly allowance; null in a sending month."
          },
          "sent_this_month": {
            "type": "integer"
          },
          "remaining_this_month": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null in a sending month."
          },
          "resets_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the daily window resets (next UTC midnight)."
          }
        },
        "required": [
          "domain",
          "plan",
          "status",
          "warmup_stage",
          "daily_cap",
          "sent_today",
          "remaining_today",
          "monthly_cap",
          "sent_this_month",
          "remaining_this_month",
          "resets_at"
        ]
      },
      "LintWarning": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "e.g. subject_empty, subject_long, subject_all_caps, spammy_phrase, no_text_part, image_only, insecure_link, missing_unsubscribe, body_large, noreply_from."
          },
          "message": {
            "type": "string"
          },
          "fix": {
            "type": "string",
            "description": "How to fix it before sending."
          }
        },
        "required": [
          "code",
          "message",
          "fix"
        ]
      },
      "SendEmailRequest": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "minLength": 3,
            "maxLength": 320,
            "description": "Sender. A bare address or \"Name <address>\". Must be on one of the account's domains or a subdomain of one."
          },
          "to": {
            "description": "One address or 1-50 addresses. Bare addresses only. Each gets its own message. Do not mix @simulator.sendforai.com with real addresses.",
            "oneOf": [
              {
                "type": "string",
                "format": "email"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "email"
                },
                "minItems": 1,
                "maxItems": 50
              }
            ]
          },
          "subject": {
            "type": "string",
            "maxLength": 998
          },
          "html": {
            "type": "string",
            "maxLength": 2000000,
            "description": "HTML body."
          },
          "text": {
            "type": "string",
            "maxLength": 2000000,
            "description": "Plain-text body."
          },
          "markdown": {
            "type": "string",
            "maxLength": 2000000,
            "description": "Markdown body. Rendered to responsive HTML plus a text part."
          },
          "reply_to": {
            "type": "string",
            "format": "email"
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Extra message headers."
          },
          "idempotency_key": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Safe to retry: the same key on the same sending domain returns the original result instead of sending again. Ignored on dry runs."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Plan without sending. Returns cost (including month_fee_usd if this would start a sending month), capacity and lint. Nothing is sent or billed."
          },
          "max_cost_usd": {
            "type": "number",
            "minimum": 0,
            "description": "Refuse with 402 budget_exceeded if the billed cost would exceed this. The first send of a month to anyone outside your own domains also needs this to be at least the $5 sending-month fee."
          },
          "stream": {
            "$ref": "#/components/schemas/Stream",
            "description": "Default transactional. broadcast adds List-Unsubscribe headers and an unsubscribe link ({{unsubscribe_url}} placeholder supported)."
          }
        },
        "required": [
          "from",
          "to",
          "subject"
        ],
        "anyOf": [
          {
            "required": [
              "html"
            ]
          },
          {
            "required": [
              "text"
            ]
          },
          {
            "required": [
              "markdown"
            ]
          }
        ],
        "description": "At least one non-empty body field (html, text or markdown) is required."
      },
      "SendEmailResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id of the first message created. null on dry runs and when every recipient was suppressed."
          },
          "dry_run": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "sent",
              "planned",
              "suppressed"
            ],
            "description": "sent: accepted for delivery. planned: dry run. suppressed: every recipient is on the suppression list, nothing sent."
          },
          "ses_message_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "recipients": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "to": {
                  "type": "string",
                  "format": "email"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "accepted",
                    "suppressed",
                    "failed"
                  ]
                },
                "reason": {
                  "type": "string",
                  "description": "Why this address was suppressed or failed."
                }
              },
              "required": [
                "to",
                "status"
              ]
            }
          },
          "cost": {
            "$ref": "#/components/schemas/Cost"
          },
          "capacity": {
            "$ref": "#/components/schemas/Capacity"
          },
          "lint": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LintWarning"
            }
          },
          "receipt": {
            "type": [
              "string",
              "null"
            ],
            "description": "HMAC receipt over id, recipients, cost and timestamp. null on dry runs, replays and simulated sends."
          },
          "simulated": {
            "type": "boolean",
            "description": "true when every recipient was @simulator.sendforai.com: nothing was delivered or billed."
          }
        },
        "required": [
          "id",
          "dry_run",
          "status",
          "ses_message_id",
          "recipients",
          "cost",
          "capacity",
          "lint",
          "receipt"
        ]
      },
      "DnsKeyRecordLike": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "e.g. _sendforai.example.com"
          },
          "type": {
            "type": "string",
            "enum": [
              "TXT"
            ]
          },
          "value": {
            "type": "string",
            "description": "e.g. v=sfa1; k=ed25519; p=<base64 key> or v=sfa1; delegate=<identity domain>"
          }
        },
        "required": [
          "name",
          "type",
          "value"
        ]
      },
      "DomainSetup": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string"
          },
          "identity_record": {
            "$ref": "#/components/schemas/DnsKeyRecordLike",
            "description": "Publish this and you have an account."
          },
          "next": {
            "type": "string",
            "description": "What to do next."
          }
        },
        "required": [
          "domain",
          "identity_record",
          "next"
        ]
      },
      "SesStatus": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "not_started",
              "pending",
              "verified",
              "failed"
            ]
          },
          "dkim_records": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "CNAME"
                  ]
                },
                "value": {
                  "type": "string"
                }
              },
              "required": [
                "name",
                "type",
                "value"
              ]
            }
          },
          "mail_from_records": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "MX",
                    "TXT"
                  ]
                },
                "value": {
                  "type": "string"
                }
              },
              "required": [
                "name",
                "type",
                "value"
              ]
            }
          }
        },
        "required": [
          "status",
          "dkim_records",
          "mail_from_records"
        ],
        "description": "Amazon SES identity state and the DNS records to publish (DomainStatus[\"ses\"] in the SDK)."
      },
      "DomainStatus": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string"
          },
          "identity": {
            "type": "object",
            "properties": {
              "found": {
                "type": "boolean"
              },
              "record": {
                "type": "string",
                "description": "The identity or delegation TXT value for this domain."
              },
              "public_key": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "null for delegated domains."
              }
            },
            "required": [
              "found",
              "record",
              "public_key"
            ]
          },
          "ses": {
            "$ref": "#/components/schemas/SesStatus"
          },
          "capacity": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Capacity"
              },
              {
                "type": "null"
              }
            ]
          },
          "next": {
            "type": "string",
            "description": "What to do next."
          }
        },
        "required": [
          "domain",
          "identity",
          "ses",
          "capacity",
          "next"
        ]
      },
      "Usage": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string"
          },
          "plan": {
            "$ref": "#/components/schemas/Plan"
          },
          "period_start": {
            "type": "string",
            "format": "date-time"
          },
          "period_end": {
            "type": "string",
            "format": "date-time"
          },
          "emails_sent": {
            "type": "integer",
            "description": "Real (non-simulated) emails this UTC month, account-wide."
          },
          "included_emails": {
            "type": "integer"
          },
          "included_remaining": {
            "type": "integer"
          },
          "ses_usd": {
            "type": "number"
          },
          "billed_usd": {
            "type": "number"
          },
          "projected_emails": {
            "type": "integer",
            "description": "Straight-line projection to period end."
          },
          "projected_billed_usd": {
            "type": "number"
          }
        },
        "required": [
          "domain",
          "plan",
          "period_start",
          "period_end",
          "emails_sent",
          "included_emails",
          "included_remaining",
          "ses_usd",
          "billed_usd",
          "projected_emails",
          "projected_billed_usd"
        ]
      },
      "BroadcastStatus": {
        "type": "string",
        "enum": [
          "draft",
          "awaiting_approval",
          "approved",
          "rejected",
          "sending",
          "sent",
          "cancelled"
        ]
      },
      "CreateBroadcastRequest": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "minLength": 3,
            "maxLength": 320,
            "description": "Sender on one of the account's domains."
          },
          "subject": {
            "type": "string",
            "minLength": 1,
            "maxLength": 998
          },
          "html": {
            "type": "string",
            "maxLength": 2000000
          },
          "markdown": {
            "type": "string",
            "maxLength": 2000000
          },
          "text": {
            "type": "string",
            "maxLength": 2000000
          },
          "filter": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "string",
                "number",
                "boolean"
              ]
            },
            "description": "Match contacts whose data contains these key/values, e.g. {\"plan\": \"pro\"}. Omit for all subscribed contacts."
          },
          "require_approval": {
            "type": "boolean",
            "description": "Default true: a human must click approval_url before it sends. false creates it already approved; then call POST /v1/broadcasts/{id}/send."
          },
          "max_cost_usd": {
            "type": "number",
            "minimum": 0,
            "description": "Refuse with 402 budget_exceeded if the estimated billed cost exceeds this."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Estimate recipients, cost and lint without creating anything. The returned id is \"dry_run\"."
          }
        },
        "required": [
          "from",
          "subject"
        ],
        "anyOf": [
          {
            "required": [
              "html"
            ]
          },
          {
            "required": [
              "text"
            ]
          },
          {
            "required": [
              "markdown"
            ]
          }
        ],
        "description": "At least one non-empty body field (html, text or markdown) is required."
      },
      "BroadcastProgress": {
        "type": "object",
        "properties": {
          "sent": {
            "type": "integer"
          },
          "queued": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "cancelled": {
            "type": "integer"
          },
          "resumes_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the remainder is waiting on tomorrow's daily cap, the time sending resumes."
          }
        },
        "required": [
          "sent",
          "queued",
          "failed",
          "cancelled",
          "resumes_at"
        ]
      },
      "Broadcast": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/BroadcastStatus"
          },
          "from": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "recipient_count": {
            "type": "integer"
          },
          "cost": {
            "$ref": "#/components/schemas/Cost"
          },
          "approval_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Hand this to a human. Clicking it approves or rejects the send. Set only while awaiting_approval."
          },
          "lint": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LintWarning"
            },
            "description": "Populated on create; empty on later reads."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "approved_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "sent_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "progress": {
            "$ref": "#/components/schemas/BroadcastProgress",
            "description": "Present once sending starts."
          }
        },
        "required": [
          "id",
          "status",
          "from",
          "subject",
          "recipient_count",
          "cost",
          "approval_url",
          "lint",
          "created_at",
          "approved_at",
          "sent_at"
        ]
      },
      "UpsertContactRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "Stored lowercased."
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "Arbitrary JSON used by broadcast filters. Replaces existing data when given."
          },
          "subscribed": {
            "type": "boolean",
            "description": "Default true for a new contact."
          }
        },
        "required": [
          "email"
        ]
      },
      "Contact": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "subscribed": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "email",
          "subscribed",
          "data",
          "created_at"
        ]
      },
      "ContactList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Contact"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as cursor to get the next page. null on the last page."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "ImportContactsRequest": {
        "type": "object",
        "properties": {
          "contacts": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10000,
            "items": {
              "$ref": "#/components/schemas/UpsertContactRequest"
            }
          }
        },
        "required": [
          "contacts"
        ]
      },
      "ImportContactsResponse": {
        "type": "object",
        "properties": {
          "imported": {
            "type": "integer",
            "description": "Contacts created or already present."
          },
          "deduplicated": {
            "type": "integer",
            "description": "Duplicate emails dropped from the request."
          }
        },
        "required": [
          "imported",
          "deduplicated"
        ]
      },
      "UnsubscribeContactRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          }
        },
        "required": [
          "email"
        ]
      },
      "EventRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "e.g. email.sent, email.delivered, email.bounced, email.complained, email.failed, broadcast.awaiting_approval, broadcast.approved, broadcast.sent, broadcast.cancelled, account.created, account.month_started, account.topped_up, account.budget_changed, account.auto_topup_changed, account.auto_topup_failed, domain.added, domain.verify_requested, contacts.imported, contact.unsubscribed."
          },
          "message_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "broadcast_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "type",
          "message_id",
          "broadcast_id",
          "data",
          "created_at"
        ]
      },
      "EventList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventRecord"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass unchanged as cursor to get the next page. null when there is nothing newer yet; keep your last non-null cursor and poll again."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Pricing": {
        "type": "object",
        "properties": {
          "currency": {
            "type": "string",
            "enum": [
              "usd"
            ]
          },
          "updated": {
            "type": "string",
            "format": "date"
          },
          "ses_per_1k_usd": {
            "type": "number"
          },
          "free": {
            "type": "object",
            "properties": {
              "monthly_emails": {
                "type": "integer"
              },
              "daily_emails": {
                "type": "integer"
              },
              "domains": {
                "type": "integer"
              },
              "recipients": {
                "type": "string",
                "enum": [
                  "own_domain"
                ],
                "description": "Free plans reach only addresses at their own sending domains."
              },
              "simulator_domain": {
                "type": "string",
                "description": "Sends to any address at this domain are simulated: free, uncapped, never delivered."
              },
              "note": {
                "type": "string"
              }
            },
            "required": [
              "monthly_emails",
              "daily_emails",
              "domains",
              "recipients",
              "simulator_domain",
              "note"
            ]
          },
          "member": {
            "type": "object",
            "properties": {
              "monthly_usd": {
                "type": "number"
              },
              "included_emails": {
                "type": "integer"
              },
              "billed": {
                "type": "string",
                "enum": [
                  "per_sending_month"
                ]
              },
              "note": {
                "type": "string"
              }
            },
            "required": [
              "monthly_usd",
              "included_emails",
              "billed",
              "note"
            ],
            "description": "Charged only in months you send to anyone outside your own domains."
          },
          "prepaid": {
            "type": "object",
            "properties": {
              "min_topup_usd": {
                "type": "number"
              },
              "default_auto_topup_usd": {
                "type": "number"
              },
              "default_monthly_budget_usd": {
                "type": "number"
              },
              "note": {
                "type": "string"
              }
            },
            "required": [
              "min_topup_usd",
              "default_auto_topup_usd",
              "default_monthly_budget_usd",
              "note"
            ]
          },
          "domains_per_account": {
            "type": "integer",
            "description": "Soft limit, raised on request."
          },
          "usage_per_1k_usd": {
            "type": "number",
            "description": "Rate for every email beyond the included allowance."
          },
          "usage_breakdown": {
            "type": "object",
            "properties": {
              "ses_usd": {
                "type": "number"
              },
              "ses_tenant_usd": {
                "type": "number"
              },
              "card_processing_usd": {
                "type": "number"
              },
              "rounding_usd": {
                "type": "number"
              },
              "note": {
                "type": "string"
              }
            },
            "required": [
              "ses_usd",
              "ses_tenant_usd",
              "card_processing_usd",
              "rounding_usd",
              "note"
            ]
          },
          "over_1m_per_month": {
            "type": "object",
            "properties": {
              "dedicated_ip_monthly_usd": {
                "type": "number"
              },
              "infra_per_1k_over_1m_usd": {
                "type": "number"
              },
              "note": {
                "type": "string"
              }
            },
            "required": [
              "dedicated_ip_monthly_usd",
              "infra_per_1k_over_1m_usd",
              "note"
            ]
          },
          "markup": {
            "type": "string"
          }
        },
        "required": [
          "currency",
          "updated",
          "ses_per_1k_usd",
          "free",
          "member",
          "prepaid",
          "domains_per_account",
          "usage_per_1k_usd",
          "usage_breakdown",
          "over_1m_per_month",
          "markup"
        ]
      },
      "BillingResponse": {
        "type": "object",
        "properties": {
          "plan": {
            "$ref": "#/components/schemas/Plan"
          },
          "subscription_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "interval": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "month",
              "year",
              null
            ]
          },
          "billing_url": {
            "type": "string",
            "format": "uri",
            "description": "Stripe billing portal link for a human. Short-lived."
          },
          "fix": {
            "type": "string"
          }
        },
        "required": [
          "plan",
          "subscription_status",
          "interval",
          "billing_url",
          "fix"
        ]
      },
      "AccountDomain": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "identity",
              "delegated"
            ],
            "description": "identity signs requests; delegated authorizes the identity via DNS."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "verified",
              "paused"
            ]
          },
          "ses_status": {
            "type": "string",
            "enum": [
              "not_started",
              "pending",
              "verified",
              "failed"
            ]
          },
          "warmup_stage": {
            "type": "integer"
          },
          "daily_cap": {
            "type": "integer",
            "description": "-1 means no cap."
          }
        },
        "required": [
          "name",
          "role",
          "status",
          "ses_status",
          "warmup_stage",
          "daily_cap"
        ]
      },
      "Account": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "plan": {
            "$ref": "#/components/schemas/Plan"
          },
          "month": {
            "type": "string",
            "description": "Current UTC month, YYYY-MM."
          },
          "month_fee_paid": {
            "type": "boolean"
          },
          "balance_usd": {
            "type": "number"
          },
          "auto_topup": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "amount_usd": {
                "type": "number"
              },
              "card_on_file": {
                "type": "boolean"
              }
            },
            "required": [
              "enabled",
              "amount_usd",
              "card_on_file"
            ]
          },
          "budget": {
            "type": "object",
            "properties": {
              "monthly_usd": {
                "type": "number"
              },
              "used_usd": {
                "type": "number"
              },
              "remaining_usd": {
                "type": "number"
              }
            },
            "required": [
              "monthly_usd",
              "used_usd",
              "remaining_usd"
            ],
            "description": "Monthly usage budget. Only a human can raise it."
          },
          "usage": {
            "type": "object",
            "properties": {
              "emails_this_month": {
                "type": "integer"
              },
              "included_remaining": {
                "type": "integer"
              }
            },
            "required": [
              "emails_this_month",
              "included_remaining"
            ]
          },
          "identity_domain": {
            "type": "string"
          },
          "domains": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDomain"
            }
          },
          "domain_limit": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "plan",
          "month",
          "month_fee_paid",
          "balance_usd",
          "auto_topup",
          "budget",
          "usage",
          "identity_domain",
          "domains",
          "domain_limit"
        ]
      },
      "DomainList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccountDomain"
            }
          },
          "limit": {
            "type": "integer",
            "description": "Sending domains allowed per account."
          }
        },
        "required": [
          "data",
          "limit"
        ]
      },
      "TopupRequest": {
        "type": "object",
        "properties": {
          "amount_usd": {
            "type": "number",
            "minimum": 10,
            "maximum": 5000,
            "default": 10,
            "description": "Default 10."
          },
          "auto_topup": {
            "type": "boolean",
            "default": false,
            "description": "Save the card and refill automatically when a send would not fit the balance. Default false."
          }
        },
        "required": []
      },
      "TopupResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "awaiting_payment"
            ]
          },
          "checkout_url": {
            "type": "string",
            "format": "uri",
            "description": "Stripe Checkout link. Hand it to your operator."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "amount_usd": {
            "type": "number"
          },
          "auto_topup": {
            "type": "boolean"
          },
          "fix": {
            "type": "string"
          }
        },
        "required": [
          "status",
          "checkout_url",
          "expires_at",
          "amount_usd",
          "auto_topup",
          "fix"
        ]
      },
      "BudgetRequest": {
        "type": "object",
        "properties": {
          "monthly_usd": {
            "type": "number",
            "minimum": 0,
            "maximum": 100000,
            "description": "New monthly usage budget in USD."
          }
        },
        "required": [
          "monthly_usd"
        ]
      },
      "BudgetResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "updated",
              "awaiting_approval"
            ]
          },
          "monthly_usd": {
            "type": "number",
            "description": "The budget now in force (unchanged while awaiting approval)."
          },
          "approval_url": {
            "type": "string",
            "format": "uri",
            "description": "Raising the budget needs a human: hand this link to your operator. Expires in 7 days."
          },
          "fix": {
            "type": "string"
          }
        },
        "required": [
          "status",
          "monthly_usd"
        ]
      },
      "AutoTopupRequest": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "amount_usd": {
            "type": "number",
            "minimum": 10,
            "maximum": 1000,
            "description": "Amount charged per automatic refill."
          }
        },
        "required": [
          "enabled"
        ]
      },
      "AddDomainRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 3,
            "maxLength": 253,
            "description": "Bare domain, e.g. news.example.org."
          }
        },
        "required": [
          "name"
        ]
      },
      "AddDomainResponse": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "added",
              "awaiting_dns"
            ]
          },
          "record": {
            "$ref": "#/components/schemas/DnsKeyRecordLike",
            "description": "The delegation record to publish (when awaiting_dns)."
          },
          "ses": {
            "$ref": "#/components/schemas/SesStatus",
            "description": "SES records (DKIM, MAIL FROM) to publish so the domain can send (when added)."
          },
          "fix": {
            "type": "string"
          }
        },
        "required": [
          "domain",
          "status",
          "fix"
        ]
      },
      "ApiIndex": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "start_here": {
            "type": "string",
            "format": "uri"
          },
          "docs_markdown": {
            "type": "string",
            "format": "uri"
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "pricing": {
            "type": "string",
            "format": "uri"
          },
          "status": {
            "type": "string",
            "format": "uri"
          },
          "mcp": {
            "type": "object",
            "properties": {
              "remote": {
                "type": "string",
                "format": "uri"
              },
              "manifest": {
                "type": "string",
                "format": "uri"
              },
              "local": {
                "type": "string"
              }
            },
            "required": [
              "remote",
              "manifest",
              "local"
            ]
          },
          "cli": {
            "type": "string"
          },
          "sdk": {
            "type": "string"
          },
          "auth": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "description",
          "start_here",
          "docs_markdown",
          "openapi",
          "pricing",
          "status",
          "mcp",
          "cli",
          "sdk",
          "auth"
        ]
      },
      "ServiceStatus": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ]
          },
          "api": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "database": {
            "type": "string",
            "enum": [
              "ok",
              "down"
            ]
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          },
          "latency_ms": {
            "type": "integer"
          },
          "contact": {
            "type": "string"
          }
        },
        "required": [
          "status",
          "api",
          "database",
          "checked_at",
          "latency_ms",
          "contact"
        ]
      }
    }
  }
}
