{
  "openapi": "3.1.0",
  "info": {
    "title": "MailWarren API",
    "version": "1.36",
    "summary": "Send transactional email, capture test email in a sandbox, and read captured email from tests and AI agents.",
    "description": "MailWarren is an email sandbox for developers. This API has three parts:\n\n* **Send API** (`POST /send`) — deliver real email from your verified sending domain. Key: a send key (`mw_live_…`) from Sending Domains.\n* **Sandbox send** (`POST /sandbox/send`) — same request body as the Send API, but the message is captured into one mailbox instead of delivered. Key: a mailbox key (`mw_sandbox_…`).\n* **Inbox API** (`/inbox/…`) — list, search, wait for, read and delete captured email, with verification codes and links already extracted. Keys: a mailbox key (`mw_sandbox_…`, one mailbox) or an account key (`mw_mcp_…`, every mailbox the account owns).\n\nAll keys go in `Authorization: Bearer <key>`. Keys are created in the MailWarren web app; there is no API for creating them.\n\nAI agents can also use the MCP server at `POST /mcp` (Model Context Protocol, streamable HTTP, JSON-RPC 2.0) with a mailbox or account key.\n\nHuman-readable docs: https://www.mailwarren.io/docs. Plain-text reference for AI agents: https://www.mailwarren.io/llms-full.txt.",
    "contact": { "name": "MailWarren", "url": "https://www.mailwarren.io/docs", "email": "support@mailwarren.io" },
    "termsOfService": "https://www.mailwarren.io/legal/terms"
  },
  "externalDocs": { "description": "MailWarren developer docs", "url": "https://www.mailwarren.io/docs" },
  "servers": [ { "url": "https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1", "description": "MailWarren API (v1)" } ],
  "tags": [
    { "name": "Send", "description": "Deliver real email from a verified sending domain." },
    { "name": "Sandbox", "description": "Capture email into a mailbox instead of delivering it." },
    { "name": "Inbox", "description": "Read captured email: list, search, wait, read, delete." },
    { "name": "Plans", "description": "Public plan catalogue and limits." },
    { "name": "MCP", "description": "Model Context Protocol server for AI agents." }
  ],
  "paths": {
    "/send": {
      "post": {
        "tags": ["Send"],
        "operationId": "sendEmail",
        "summary": "Send an email",
        "description": "Delivers a message through Amazon SES from the sending domain your key was created for. `From` must be an address on that domain.\n\nA well-formed request always returns HTTP 200; read `status` for the outcome. Your account's first send is held for a one-time manual review (`PendingReview`). Recipients on your suppression list (after a hard bounce or complaint) are skipped. Sending uses prepaid credit; without enough credit the message is held for up to 7 days (`HeldForCredit`) and sent automatically once credit is added.\n\nError responses (400/401/403) have a plain-text body containing the message, except malformed JSON (`{\"error\":\"invalid_json\"}`) and schema validation failures (`{\"error\":\"validation_failed\",\"details\":\"…\"}`).",
        "security": [ { "sendKey": [] } ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" },
            "examples": {
              "plain": { "summary": "Text email", "value": { "From": "noreply@yourdomain.com", "To": ["customer@example.com"], "Subject": "Welcome!", "TextBody": "Thanks for signing up." } },
              "inlineImage": { "summary": "HTML with an inline logo", "value": { "From": "noreply@yourdomain.com", "To": ["customer@example.com"], "Subject": "Your receipt", "HtmlBody": "<img src=\"cid:logo\" alt=\"Acme\"><p>Thanks for your order.</p>", "TextBody": "Thanks for your order.", "Attachments": [ { "FileName": "logo.png", "ContentType": "image/png", "ContentBase64": "iVBORw0KGgo…", "ContentId": "logo" } ] } }
            } } }
        },
        "responses": {
          "200": { "description": "The request was accepted; `status` says what happened.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendResponse" } } } },
          "400": { "$ref": "#/components/responses/PlainTextError" },
          "401": { "$ref": "#/components/responses/PlainTextError" },
          "403": { "description": "`From` is not on the domain this key was created for (plain-text message).", "content": { "text/plain": { "schema": { "type": "string" } } } }
        }
      }
    },
    "/sandbox/send": {
      "post": {
        "tags": ["Sandbox"],
        "operationId": "sandboxSend",
        "summary": "Capture an email into a mailbox (nothing is delivered)",
        "description": "Same request body as `POST /send`, but the message is captured into the one mailbox the key belongs to. `From` and `To` can be anything — they are stored for display only. No domain check, review queue or daily cap. Counts against the account's monthly capture allowance (429 when it is used up).\n\nThe success response deliberately matches the Send API (`status: \"Sent\"`, `sesMessageId` set to the captured message id) so the same client code works against both. Attachment `ContentId`/`Disposition` follow exactly the same rules as the Send API.",
        "security": [ { "mailboxKey": [] } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } } } },
        "responses": {
          "200": { "description": "Captured.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendResponse" } } } },
          "400": { "$ref": "#/components/responses/PlainTextError" },
          "401": { "$ref": "#/components/responses/PlainTextError" },
          "429": { "description": "Monthly capture allowance used up (plain-text message).", "content": { "text/plain": { "schema": { "type": "string" } } } }
        }
      }
    },
    "/inbox/mailboxes": {
      "get": {
        "tags": ["Inbox"],
        "operationId": "listMailboxes",
        "summary": "List the mailboxes this key can read",
        "security": [ { "mailboxKey": [] }, { "accountKey": [] } ],
        "responses": {
          "200": { "description": "Mailboxes.", "content": { "application/json": { "schema": {
            "type": "object", "required": ["keyKind", "mailboxes"],
            "properties": {
              "keyKind": { "type": "string", "enum": ["mailbox", "account"], "description": "`mailbox` for a mw_sandbox_ key, `account` for a mw_mcp_ key." },
              "mailboxes": { "type": "array", "items": { "$ref": "#/components/schemas/Mailbox" } }
            } } } } },
          "401": { "$ref": "#/components/responses/JsonError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/inbox/messages": {
      "get": {
        "tags": ["Inbox"],
        "operationId": "listMessages",
        "summary": "List or search captured messages (newest first)",
        "description": "Text filters are case-insensitive \"contains\" matches. No default `since` here — a plain listing shows what is in the mailbox.",
        "security": [ { "mailboxKey": [] }, { "accountKey": [] } ],
        "parameters": [
          { "$ref": "#/components/parameters/MailboxId" },
          { "$ref": "#/components/parameters/To" },
          { "$ref": "#/components/parameters/From" },
          { "$ref": "#/components/parameters/Subject" },
          { "$ref": "#/components/parameters/Text" },
          { "$ref": "#/components/parameters/Since" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 20 } }
        ],
        "responses": {
          "200": { "description": "Message summaries.", "content": { "application/json": { "schema": {
            "type": "object", "required": ["messages"],
            "properties": { "messages": { "type": "array", "items": { "$ref": "#/components/schemas/MessageSummary" } } } } } } },
          "400": { "$ref": "#/components/responses/JsonError" },
          "401": { "$ref": "#/components/responses/JsonError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Inbox"],
        "operationId": "clearMailbox",
        "summary": "Delete every message in one mailbox",
        "description": "Permanently deletes every message and attachment in the mailbox; the mailbox and its keys stay. A mailbox key clears its own mailbox and needs no `mailbox_id` (naming a different mailbox is a 404). An account key must pass `mailbox_id`. Does not give back monthly capture allowance.",
        "security": [ { "mailboxKey": [] }, { "accountKey": [] } ],
        "parameters": [ { "$ref": "#/components/parameters/MailboxId" } ],
        "responses": {
          "200": { "description": "How many messages were deleted.", "content": { "application/json": { "schema": {
            "type": "object", "required": ["deleted"], "properties": { "deleted": { "type": "integer" } } } } } },
          "400": { "$ref": "#/components/responses/JsonError" },
          "401": { "$ref": "#/components/responses/JsonError" },
          "404": { "$ref": "#/components/responses/JsonError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/inbox/messages/{id}": {
      "get": {
        "tags": ["Inbox"],
        "operationId": "getMessage",
        "summary": "Read one message in full, with extracted codes and links",
        "description": "`analyze=true` adds the pre-send checks report (`analysis`): content problems, spam-risk score, Gmail/Outlook/Apple Mail/Yahoo compatibility, accessibility, phones, dark mode and static link checks — on every plan. `links=live` also requests each link (up to 15; never private addresses or single-use verify/reset/invite/login/unsubscribe links) and implies `analyze`; it is slower (up to ~18 s) and plan-gated: 403 `plan_limit` when the plan doesn't include it, 429 `quota_exceeded` when today's quota is used up.",
        "security": [ { "mailboxKey": [] }, { "accountKey": [] } ],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "analyze", "in": "query", "schema": { "type": "string", "enum": ["true", "false"] } },
          { "name": "links", "in": "query", "schema": { "type": "string", "enum": ["live"] } }
        ],
        "responses": {
          "200": { "description": "The message.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Message" } } } },
          "400": { "$ref": "#/components/responses/JsonError" },
          "401": { "$ref": "#/components/responses/JsonError" },
          "403": { "$ref": "#/components/responses/PlanDenied" },
          "404": { "$ref": "#/components/responses/JsonError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/inbox/wait": {
      "get": {
        "tags": ["Inbox"],
        "operationId": "waitForMessage",
        "summary": "Long-poll until a matching message arrives",
        "description": "Holds the request open (polling about every 700 ms) until a message matching the filters arrives, then returns the NEWEST match in full. One request waits at most 25 s; if nothing arrived it returns `matched: false` — call again. `since` defaults to 10 minutes ago so an old message can't satisfy a new wait; pass an explicit `since` taken just before the action you are testing, or clear the mailbox first. Counts as one request against the per-minute limit however long it is held.",
        "security": [ { "mailboxKey": [] }, { "accountKey": [] } ],
        "parameters": [
          { "$ref": "#/components/parameters/MailboxId" },
          { "$ref": "#/components/parameters/To" },
          { "$ref": "#/components/parameters/From" },
          { "$ref": "#/components/parameters/Subject" },
          { "$ref": "#/components/parameters/Text" },
          { "$ref": "#/components/parameters/Since" },
          { "name": "timeout", "in": "query", "description": "Seconds to wait.", "schema": { "type": "integer", "minimum": 1, "maximum": 25, "default": 20 } }
        ],
        "responses": {
          "200": { "description": "Either the matching message or a timeout marker.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WaitResponse" } } } },
          "400": { "$ref": "#/components/responses/JsonError" },
          "401": { "$ref": "#/components/responses/JsonError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/inbox/attachments/{id}": {
      "get": {
        "tags": ["Inbox"],
        "operationId": "getAttachment",
        "summary": "Download one attachment (base64 in JSON)",
        "description": "Attachment ids come from the `attachments` array of a message.",
        "security": [ { "mailboxKey": [] }, { "accountKey": [] } ],
        "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ],
        "responses": {
          "200": { "description": "The attachment.", "content": { "application/json": { "schema": {
            "type": "object", "required": ["fileName", "contentType", "sizeBytes", "contentBase64"],
            "properties": {
              "fileName": { "type": "string" },
              "contentType": { "type": "string" },
              "sizeBytes": { "type": "integer" },
              "contentBase64": { "type": "string", "contentEncoding": "base64" }
            } } } } },
          "400": { "$ref": "#/components/responses/JsonError" },
          "401": { "$ref": "#/components/responses/JsonError" },
          "404": { "$ref": "#/components/responses/JsonError" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/plans": {
      "get": {
        "tags": ["Plans"],
        "operationId": "listPlans",
        "summary": "The plans on offer and their limits (no key needed)",
        "security": [],
        "responses": {
          "200": { "description": "Plans in catalogue order.", "content": { "application/json": { "schema": {
            "type": "object", "required": ["plans"],
            "properties": { "plans": { "type": "array", "items": { "$ref": "#/components/schemas/Plan" } } } } } } }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": ["MCP"],
        "operationId": "mcp",
        "summary": "Model Context Protocol server for AI agents (JSON-RPC 2.0)",
        "description": "A remote MCP server (streamable HTTP transport, stateless). Configure your MCP client with this URL and `Authorization: Bearer <mailbox or account key>`; the client handles the JSON-RPC exchange (`initialize`, `tools/list`, `tools/call`, `prompts/list`, …).\n\nTools: `list_mailboxes`, `list_warrens`, `list_messages`, `wait_for_message`, `get_message`, `analyze_message` (read-only); `send_test_email` (writes into a mailbox, never delivers); `delete_all_messages` (destructive). Each tool carries MCP safety annotations. Prompts: verify a signup email, review an email's quality, find a missing email. Same keys, limits and search as the Inbox API.",
        "security": [ { "mailboxKey": [] }, { "accountKey": [] } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "description": "A JSON-RPC 2.0 request.", "required": ["jsonrpc", "method"],
          "properties": { "jsonrpc": { "const": "2.0" }, "id": { "type": ["string", "integer"] }, "method": { "type": "string", "examples": ["tools/list"] }, "params": { "type": "object" } } },
          "example": { "jsonrpc": "2.0", "id": 1, "method": "tools/list" } } } },
        "responses": { "200": { "description": "A JSON-RPC 2.0 response.", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "sendKey": { "type": "http", "scheme": "bearer", "description": "Send key (`mw_live_…`), created on the Sending Domains page for one verified domain. Only for `POST /send`." },
      "mailboxKey": { "type": "http", "scheme": "bearer", "description": "Mailbox key (`mw_sandbox_…`), created on a mailbox's page in a Warren. Reads and writes that one mailbox only." },
      "accountKey": { "type": "http", "scheme": "bearer", "description": "Account key (`mw_mcp_…`), created under Account → MCP keys. Reads every mailbox the account owns; pass `mailbox_id` to narrow. Keep it out of client-side code." }
    },
    "parameters": {
      "MailboxId": { "name": "mailbox_id", "in": "query", "description": "Restrict to one mailbox. Required for an account key when clearing; a mailbox key is always pinned to its own mailbox.", "schema": { "type": "string", "format": "uuid" } },
      "To": { "name": "to", "in": "query", "description": "Recipient contains (case-insensitive).", "schema": { "type": "string" } },
      "From": { "name": "from", "in": "query", "description": "Sender contains (case-insensitive).", "schema": { "type": "string" } },
      "Subject": { "name": "subject", "in": "query", "description": "Subject contains (case-insensitive).", "schema": { "type": "string" } },
      "Text": { "name": "text", "in": "query", "description": "Subject, text body or HTML body contains (case-insensitive).", "schema": { "type": "string" } },
      "Since": { "name": "since", "in": "query", "description": "Only mail received after this: an ISO-8601 time or an age such as `30s`, `5m`, `2h`, `1d` (an age avoids clock differences).", "schema": { "type": "string", "examples": ["5m", "2026-09-24T10:00:00Z"] } }
    },
    "responses": {
      "PlainTextError": { "description": "The request was refused; the body is the reason as plain text (malformed JSON returns `{\"error\":\"invalid_json\"}`).", "content": { "text/plain": { "schema": { "type": "string" } } } },
      "JsonError": { "description": "Error.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": {
        "description": "The account's per-minute request limit was reached (`code: rate_limited`), or — on a live link check — today's quota is used up (`code: quota_exceeded`, resets 00:00 UTC). Honour `Retry-After` for `rate_limited`; retrying won't help `quota_exceeded`.",
        "headers": { "Retry-After": { "description": "Seconds to wait.", "schema": { "type": "integer" } } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlanDenial" } } }
      },
      "PlanDenied": { "description": "The plan doesn't include this feature (`code: plan_limit`, live link checks on Free).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlanDenial" } } } }
    },
    "schemas": {
      "SendRequest": {
        "type": "object",
        "required": ["From", "To", "Subject"],
        "properties": {
          "From": { "type": "string", "description": "Send API: must be an address on your key's verified domain. Sandbox: display only." },
          "To": { "type": "array", "items": { "type": "string" }, "minItems": 1, "description": "Send API: up to your per-message recipient cap (50 by default)." },
          "Subject": { "type": "string" },
          "TextBody": { "type": "string", "description": "Set TextBody, HtmlBody or both." },
          "HtmlBody": { "type": "string", "description": "Sent exactly as given; inline images are referenced as `<img src=\"cid:logo\">`." },
          "Attachments": { "type": "array", "maxItems": 10, "items": { "$ref": "#/components/schemas/Attachment" }, "description": "Up to 10 files including inline images. Send API: whole message under 10 MB after base64. Sandbox: attachments under 30 MB combined." }
        }
      },
      "Attachment": {
        "type": "object",
        "required": ["FileName", "ContentType", "ContentBase64"],
        "properties": {
          "FileName": { "type": "string" },
          "ContentType": { "type": "string", "examples": ["application/pdf", "image/png"] },
          "ContentBase64": { "type": "string", "contentEncoding": "base64", "description": "The raw file bytes, base64-encoded." },
          "ContentId": { "type": "string", "maxLength": 250, "description": "Makes this an inline image referenced from HtmlBody as `cid:<ContentId>`. `logo`, `<logo>` and `cid:logo` are equivalent. Printable ASCII, no spaces, quotes or angle brackets; unique within the message (case-insensitive)." },
          "Disposition": { "type": "string", "enum": ["inline", "attachment"], "description": "Defaults to `inline` when ContentId is set, otherwise `attachment`. An inline item needs a ContentId and an HtmlBody — otherwise the request is refused with 400." }
        }
      },
      "SendResponse": {
        "type": "object",
        "required": ["status"],
        "properties": {
          "status": { "type": "string", "enum": ["Sent", "PendingReview", "Rejected", "Failed", "HeldForCredit"], "description": "Sent: handed to SES. PendingReview: your first send is held for a one-time review. Rejected: suspended, daily cap reached, or every recipient suppressed (see message). Failed: SES refused it (see message; any credit charged is refunded). HeldForCredit: not enough credit — held up to 7 days and sent once credit is added. The sandbox always answers Sent." },
          "sesMessageId": { "type": ["string", "null"], "description": "SES message id when Sent; the captured message id for the sandbox." },
          "message": { "type": ["string", "null"], "description": "Why, when the status isn't a plain success (also lists skipped suppressed recipients)." }
        }
      },
      "Mailbox": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "warrenId": { "type": "string", "format": "uuid", "description": "The Warren (project) the mailbox belongs to." },
          "warrenName": { "type": "string" },
          "messageCount": { "type": "integer" }
        }
      },
      "MessageSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "mailboxId": { "type": "string", "format": "uuid" },
          "fromAddress": { "type": "string" },
          "toAddresses": { "type": "array", "items": { "type": "string" } },
          "subject": { "type": ["string", "null"] },
          "receivedAtUtc": { "type": "string", "format": "date-time" },
          "attachmentCount": { "type": "integer" }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "mailboxId": { "type": "string", "format": "uuid" },
          "fromAddress": { "type": "string" },
          "toAddresses": { "type": "array", "items": { "type": "string" } },
          "subject": { "type": ["string", "null"] },
          "receivedAtUtc": { "type": "string", "format": "date-time" },
          "bodyText": { "type": ["string", "null"] },
          "bodyHtml": { "type": ["string", "null"] },
          "headers": { "type": ["string", "null"], "description": "The raw header block." },
          "attachments": { "type": "array", "items": { "$ref": "#/components/schemas/AttachmentInfo" } },
          "code": { "type": ["string", "null"], "description": "The most likely verification code — only set at high confidence (a word like code/OTP/PIN/verification just before it), so an order number is never returned here." },
          "codes": { "type": "array", "items": { "$ref": "#/components/schemas/CodeCandidate" } },
          "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } },
          "analysis": { "oneOf": [ { "$ref": "#/components/schemas/Analysis" }, { "type": "null" } ], "description": "Only with analyze=true or links=live." }
        }
      },
      "AttachmentInfo": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "fileName": { "type": "string" },
          "contentType": { "type": "string" },
          "sizeBytes": { "type": "integer" },
          "contentId": { "type": ["string", "null"], "description": "Content-ID without angle brackets — what the HTML refers to as `cid:…`." },
          "disposition": { "type": "string", "enum": ["attachment", "inline"] }
        }
      },
      "CodeCandidate": {
        "type": "object",
        "properties": {
          "value": { "type": "string" },
          "confidence": { "type": "string", "enum": ["high", "low"] },
          "context": { "type": "string", "description": "The text around the code." }
        }
      },
      "Link": {
        "type": "object",
        "properties": {
          "url": { "type": "string" },
          "text": { "type": ["string", "null"] },
          "purpose": { "type": "string", "enum": ["verify", "reset", "invite", "login", "unsubscribe", "other"], "description": "Guessed from the URL and link text." }
        }
      },
      "WaitResponse": {
        "type": "object",
        "required": ["matched"],
        "properties": {
          "matched": { "type": "boolean" },
          "message": { "$ref": "#/components/schemas/Message", "description": "Present when matched is true." },
          "timedOut": { "type": "boolean", "description": "True when nothing matched in time — call again." },
          "waitedSeconds": { "type": "integer" },
          "since": { "type": "string", "format": "date-time", "description": "The cut-off that was applied." }
        }
      },
      "Analysis": {
        "type": "object",
        "properties": {
          "errors": { "type": "integer" },
          "warnings": { "type": "integer" },
          "infos": { "type": "integer" },
          "issues": { "type": "array", "items": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "category": { "type": "string", "enum": ["content", "deliverability", "compatibility", "accessibility", "mobile", "links"] },
              "severity": { "type": "string", "enum": ["error", "warning", "info"] },
              "message": { "type": "string" },
              "clients": { "type": ["array", "null"], "items": { "type": "string" }, "description": "Mail clients the finding is about." }
            } } },
          "spam": { "type": "object", "description": "A content-only estimate on SpamAssassin's scale; it cannot see sender reputation, SPF, DKIM or DMARC.",
            "properties": {
              "score": { "type": "number" },
              "threshold": { "type": "number" },
              "verdict": { "type": "string", "enum": ["low-risk", "borderline", "likely-spam"] },
              "rules": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "points": { "type": "number" }, "description": { "type": "string" } } } },
              "note": { "type": "string" }
            } },
          "compatibility": { "type": "array", "items": { "type": "object",
            "properties": {
              "client": { "type": "string", "examples": ["Gmail", "Outlook (Windows)", "Apple Mail", "Yahoo Mail"] },
              "status": { "type": "string", "enum": ["ok", "minor", "issues", "problems"] },
              "problems": { "type": ["array", "null"], "items": { "type": "string" } }
            } } },
          "links": { "type": ["object", "null"], "description": "Only after links=live.",
            "properties": {
              "total": { "type": "integer" },
              "checked": { "type": "integer" },
              "broken": { "type": "integer" },
              "results": { "type": "array", "items": { "type": "object",
                "properties": {
                  "url": { "type": "string" },
                  "text": { "type": ["string", "null"] },
                  "purpose": { "type": "string" },
                  "status": { "type": "string", "enum": ["ok", "broken", "blocked", "restricted", "skipped", "error"] },
                  "httpStatus": { "type": ["integer", "null"] },
                  "finalUrl": { "type": ["string", "null"] },
                  "note": { "type": ["string", "null"] }
                } } }
            } }
        }
      },
      "Plan": {
        "type": "object",
        "properties": {
          "planCode": { "type": "string" },
          "name": { "type": "string" },
          "priceUsd": { "type": "number", "description": "Per month." },
          "purchasable": { "type": "boolean" },
          "features": { "type": "array", "items": { "type": "string" } },
          "limits": { "type": "object", "description": "null = unlimited.",
            "properties": {
              "monthlyCaptureCap": { "type": ["integer", "null"], "description": "2147483647 = no cap." },
              "mcpKeyLimit": { "type": ["integer", "null"] },
              "teamSeatLimit": { "type": ["integer", "null"] },
              "messageRetentionDays": { "type": ["integer", "null"] },
              "apiRequestsPerMinute": { "type": ["integer", "null"], "description": "Inbox API + MCP requests per account per minute." },
              "liveLinkChecksPerDay": { "type": ["integer", "null"], "description": "0 = not included." }
            } }
        }
      },
      "Error": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string" } } },
      "PlanDenial": {
        "type": "object",
        "required": ["error", "code"],
        "properties": {
          "error": { "type": "string" },
          "code": { "type": "string", "enum": ["rate_limited", "plan_limit", "quota_exceeded"] },
          "plan": { "type": "string" },
          "limit": { "type": ["integer", "null"] },
          "retryAfterSeconds": { "type": ["integer", "null"] },
          "usedToday": { "type": ["integer", "null"] }
        }
      }
    }
  }
}
