{
  "openapi": "3.1.0",
  "info": {
    "title": "Mailery API",
    "version": "0.1.0",
    "description": "Mailery API for accounts, API keys, billing, hosted mailboxes, messages, digests, domains, provider webhooks, and admin operations. All endpoints are versioned under /api/v1; per-endpoint rate limits, the versioning/deprecation policy, and the contract changelog live in docs/API.md and docs/CHANGELOG.md."
  },
  "servers": [
    {
      "url": "https://mailery.co",
      "description": "Mailery"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Public"
    },
    {
      "name": "Auth"
    },
    {
      "name": "Account"
    },
    {
      "name": "API Keys"
    },
    {
      "name": "Feedback"
    },
    {
      "name": "Billing"
    },
    {
      "name": "Mailboxes"
    },
    {
      "name": "Messages"
    },
    {
      "name": "Digests"
    },
    {
      "name": "Domains"
    },
    {
      "name": "Webhooks"
    },
    {
      "name": "Admin"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "session-token-or-mailery-api-key"
      },
      "adminBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "MAILERY_ADMIN_API_KEY"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "details": {}
            }
          }
        }
      },
      "AdminBillingAnomaliesRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "minutes": {
            "type": "number",
            "minimum": 0,
            "description": "Alias for olderThanMinutes. Only flag events older than this many minutes."
          },
          "olderThanMinutes": {
            "type": "number",
            "minimum": 0,
            "description": "Only flag events older than this many minutes."
          },
          "limit": {
            "type": "number",
            "minimum": 1,
            "maximum": 1000,
            "description": "Maximum stripe_events rows to report."
          },
          "subscriptionLimit": {
            "type": "number",
            "minimum": 1,
            "maximum": 500,
            "description": "Maximum local subscriptions to cross-check against Stripe."
          },
          "predeploy": {
            "type": "boolean",
            "description": "Also fail when legacy positive credit balances need authoritative reconciliation."
          }
        }
      },
      "TenantFreezeRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "reason": {
            "type": "string",
            "description": "Suspension reason recorded on the tenant and audit log. Defaults to operator_freeze."
          }
        }
      },
      "TenantUnfreezeRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "clearBillingFlag": {
            "type": "boolean",
            "description": "Defaults to true. Pass false to keep billing_flag in place."
          }
        }
      },
      "PlatformInfo": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "service": {
            "const": "platform-mailery"
          },
          "stripe_mode": {
            "enum": [
              "test",
              "live"
            ]
          },
          "mail_mode": {
            "enum": [
              "test",
              "live"
            ]
          },
          "release_sha": {
            "type": "string"
          },
          "shared_domain": {
            "type": [
              "string",
              "null"
            ],
            "description": "Shared Mailery-owned sending domain for the Free tier (MAILERY_SHARED_DOMAIN); null when the deployment has none configured."
          },
          "domain_boundary_contract": {
            "$ref": "#/components/schemas/DomainBoundaryContract"
          }
        }
      },
      "HealthInfo": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PlatformInfo"
          },
          {
            "type": "object",
            "required": [
              "status",
              "db"
            ],
            "properties": {
              "status": {
                "enum": [
                  "healthy",
                  "degraded"
                ]
              },
              "db": {
                "type": "object",
                "description": "Result of a short-deadline database ping.",
                "required": [
                  "ok"
                ],
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "latency_ms": {
                    "type": "integer",
                    "description": "Ping latency; present when ok is true."
                  },
                  "error": {
                    "type": "string",
                    "description": "Ping failure reason; present when ok is false."
                  }
                }
              }
            }
          }
        ]
      },
      "DomainKind": {
        "type": "string",
        "enum": [
          "system_domain",
          "tenant_domain",
          "self_hosted_domain"
        ]
      },
      "DomainBoundaryContract": {
        "type": "object",
        "description": "Platform domain boundary contract. mailery.co is a system_domain; customer domains are tenant_domain records; external domains outside hosted tenant control use self_hosted_domain records. DMARC gates outbound ramp only and must not block non-sending aggregation.",
        "required": [
          "system_domain",
          "tenant_domain",
          "self_hosted_domain",
          "invariants"
        ],
        "properties": {
          "system_domain": {
            "type": "object",
            "required": [
              "kind",
              "domain",
              "owner",
              "purpose",
              "tenant_assignable"
            ],
            "properties": {
              "kind": {
                "const": "system_domain"
              },
              "domain": {
                "type": "string"
              },
              "owner": {
                "const": "platform"
              },
              "purpose": {
                "type": "string"
              },
              "tenant_assignable": {
                "const": false
              }
            }
          },
          "tenant_domain": {
            "type": "object",
            "required": [
              "kind",
              "owner",
              "purpose",
              "readiness_axes",
              "dmarc_gate"
            ],
            "properties": {
              "kind": {
                "const": "tenant_domain"
              },
              "owner": {
                "const": "tenant"
              },
              "purpose": {
                "type": "string"
              },
              "readiness_axes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "dmarc_gate": {
                "const": "outbound_ramp_only"
              }
            }
          },
          "self_hosted_domain": {
            "type": "object",
            "required": [
              "kind",
              "owner",
              "purpose",
              "platform_controlled"
            ],
            "properties": {
              "kind": {
                "const": "self_hosted_domain"
              },
              "owner": {
                "const": "self_hosted_operator"
              },
              "purpose": {
                "type": "string"
              },
              "platform_controlled": {
                "const": false
              }
            }
          },
          "invariants": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "TenantDomain": {
        "type": "object",
        "required": [
          "id",
          "domain",
          "kind",
          "status",
          "provider",
          "readiness",
          "enabled"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "domain": {
            "type": "string"
          },
          "kind": {
            "$ref": "#/components/schemas/DomainKind"
          },
          "status": {
            "enum": [
              "planned",
              "pending",
              "active",
              "restricted",
              "suspended",
              "deleted"
            ]
          },
          "provider": {
            "type": "string"
          },
          "readiness": {
            "type": "object",
            "properties": {
              "ownership": {
                "type": "string"
              },
              "dns": {
                "type": "string"
              },
              "inbound": {
                "type": "string"
              },
              "outbound": {
                "type": "string"
              },
              "dmarc": {
                "type": "string"
              },
              "events": {
                "type": "string"
              },
              "billing": {
                "type": "string"
              }
            }
          },
          "enabled": {
            "type": "object",
            "properties": {
              "inbound": {
                "type": "boolean"
              },
              "outbound": {
                "type": "boolean"
              }
            }
          },
          "dmarc_policy": {
            "$ref": "#/components/schemas/DmarcPolicy"
          }
        }
      },
      "TenantDomainDnsRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "value": {
            "type": "string"
          },
          "ttl": {
            "type": "integer"
          },
          "priority": {
            "type": [
              "integer",
              "null"
            ]
          },
          "purpose": {
            "type": "string"
          },
          "status": {
            "type": "string"
          }
        }
      },
      "TenantDomainEvent": {
        "type": "object",
        "required": [
          "id",
          "event",
          "actor_type",
          "metadata",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "event": {
            "type": "string"
          },
          "actor_type": {
            "type": "string"
          },
          "actor_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "from_state": {
            "type": [
              "object",
              "null"
            ]
          },
          "to_state": {
            "type": [
              "object",
              "null"
            ]
          },
          "metadata": {
            "type": "object"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DmarcPolicy": {
        "type": "object",
        "required": [
          "status",
          "policy",
          "record",
          "report_uri",
          "report_count",
          "aligned_count",
          "failed_count",
          "fail_rate",
          "clean_streak",
          "recommendation",
          "can_apply_enforcement",
          "outbound_gate",
          "inbound_gate"
        ],
        "properties": {
          "status": {
            "type": "string"
          },
          "policy": {
            "enum": [
              "none",
              "quarantine",
              "reject"
            ]
          },
          "record": {
            "type": "string"
          },
          "report_uri": {
            "type": "string"
          },
          "report_count": {
            "type": "integer"
          },
          "aligned_count": {
            "type": "integer"
          },
          "failed_count": {
            "type": "integer"
          },
          "fail_rate": {
            "type": "number"
          },
          "clean_streak": {
            "type": "integer"
          },
          "last_report_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "recommendation": {
            "enum": [
              "collect_reports",
              "eligible_for_quarantine",
              "eligible_for_reject"
            ]
          },
          "can_apply_enforcement": {
            "type": "boolean"
          },
          "outbound_gate": {
            "enum": [
              "monitoring",
              "enforcement_ready",
              "blocked"
            ]
          },
          "inbound_gate": {
            "const": false
          }
        }
      },
      "DmarcReportRequest": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string"
          },
          "reportId": {
            "type": "string"
          },
          "xml": {
            "type": "string"
          },
          "rawXml": {
            "type": "string"
          },
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "aligned": {
            "type": "integer",
            "minimum": 0
          },
          "failed": {
            "type": "integer",
            "minimum": 0
          },
          "policy": {
            "type": "string"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DmarcPolicyRequest": {
        "type": "object",
        "properties": {
          "action": {
            "enum": [
              "recommend",
              "apply"
            ]
          },
          "policy": {
            "enum": [
              "quarantine",
              "reject"
            ]
          }
        }
      },
      "SignupRequest": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string",
            "minLength": 8
          },
          "name": {
            "type": "string"
          },
          "inviteCode": {
            "type": "string"
          }
        }
      },
      "LoginRequest": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string"
          }
        }
      },
      "AuthResult": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string"
          },
          "user": {
            "$ref": "#/components/schemas/User"
          },
          "tenant": {
            "$ref": "#/components/schemas/Tenant"
          }
        }
      },
      "Tenant": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "plan": {
            "type": "string"
          }
        }
      },
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tenantId": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          },
          "role": {
            "type": "string"
          },
          "emailVerifiedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ApiKeyRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "prefix": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "enum": [
                "full",
                "mail_read",
                "mail_write",
                "billing_read",
                "domains:purchase"
              ]
            },
            "description": "Tenant-mintable scopes. New keys default to least privilege ['mail_read','mail_write'] when none are specified. 'domains:purchase' gates the irreversible domain-purchase path and must be requested explicitly (or covered by 'full'). 'admin' is not selectable; legacy keys carrying it are aliased to 'full'."
          },
          "lastUsedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revokedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Mailbox": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          },
          "provider": {
            "enum": [
              "manual",
              "resend",
              "hosted",
              "gmail",
              "sandbox"
            ]
          },
          "status": {
            "type": "string"
          }
        }
      },
      "MessageListPage": {
        "type": "object",
        "description": "Lightweight, bodyless message page. Excludes text_body, html_body, clean_markdown, raw_email, metadata and classification (see GET /messages/{id}).",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MessageListItem"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full-microsecond (sort_at,id) keyset cursor, or null on the last page."
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "MessageListItem": {
        "type": "object",
        "description": "Bodyless list item. snake_case and camelCase are both emitted; snake_case shown here.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "tenant_id": {
            "type": "string",
            "format": "uuid"
          },
          "mailbox_id": {
            "type": "string",
            "format": "uuid"
          },
          "thread_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "subject": {
            "type": "string"
          },
          "from_address": {
            "type": "string"
          },
          "to_addresses": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "snippet": {
            "type": "string",
            "description": "Maintained <=200 char preview; never detoasts the body columns."
          },
          "is_read": {
            "type": "boolean"
          },
          "is_important": {
            "type": "boolean"
          },
          "is_spam": {
            "type": "boolean"
          },
          "is_trash": {
            "type": "boolean"
          },
          "is_archived": {
            "type": "boolean"
          },
          "is_starred": {
            "type": "boolean",
            "description": "Real star flag, distinct from is_important. Backs the 'starred' folder view."
          },
          "importance_score": {
            "type": "integer"
          },
          "received_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "sort_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Exact per-row change watermark. A GET /messages/changes client advances its watermark from each row's updated_at (also emitted as updatedAt)."
          },
          "has_attachments": {
            "type": "boolean"
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "label_records": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Label"
            }
          }
        }
      },
      "MessageChangesPage": {
        "type": "object",
        "description": "Delta / JMAP-changesSince page. Same bodyless items as MessageListPage, ordered by updated_at ASC, plus a server-issued watermark.",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MessageListItem"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full-microsecond (updated_at,id) keyset cursor, or null on the last page."
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "server_time": {
            "type": "string",
            "format": "date-time",
            "description": "Server clock captured before the read. Also returned as the X-Mailery-Server-Time header. Once fully drained (next_cursor null), a client may adopt this as its next updatedSince watermark."
          },
          "serverTime": {
            "type": "string",
            "format": "date-time"
          },
          "state": {
            "type": "string",
            "description": "Opaque base64url state token (encodes server_time)."
          }
        }
      },
      "Label": {
        "type": "object",
        "description": "Tenant label with its per-label live-message count. System-label counts come from the O(1) folder counters (message_counters); custom-label counts are computed from message_labels joined to live messages. count is also emitted as message_count / messageCount.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "system",
              "custom"
            ]
          },
          "count": {
            "type": "integer",
            "description": "Number of live (non-deleted) messages for this label: the folder counter for a system label, the message_labels join count for a custom label."
          },
          "message_count": {
            "type": "integer"
          },
          "messageCount": {
            "type": "integer"
          }
        }
      },
      "BulkMessageRequest": {
        "type": "object",
        "description": "Bounded, tenant-scoped, counter-correct bulk mutation. Provide either an explicit id list OR a { mailboxId, folder } filter, plus an action.",
        "required": [
          "action"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "markRead",
              "markUnread",
              "important",
              "unimportant",
              "star",
              "unstar",
              "archive",
              "unarchive",
              "spam",
              "unspam",
              "trash",
              "untrash",
              "addLabel",
              "removeLabel",
              "delete"
            ]
          },
          "ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Explicit id list (<= 1000). Mutually exclusive with the mailboxId/folder filter."
          },
          "mailboxId": {
            "type": "string",
            "format": "uuid",
            "description": "Filter path: scope the action to one mailbox. Alias: mailbox_id."
          },
          "folder": {
            "type": "string",
            "description": "Filter path: restrict to a folder/group. Alias: group."
          },
          "label": {
            "type": "string",
            "description": "Required for addLabel/removeLabel."
          },
          "cursor": {
            "type": "string",
            "description": "Resume token (filter path only): pass back a prior next_cursor to continue draining a large folder."
          }
        }
      },
      "BulkMessageResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "action": {
            "type": "string"
          },
          "affected": {
            "type": "integer",
            "description": "Rows that actually transitioned (not merely matched)."
          },
          "matched": {
            "type": "integer"
          },
          "has_more": {
            "type": "boolean",
            "description": "Filter path only: true if a resume cursor is returned."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "MessageUpload": {
        "type": "object",
        "required": [
          "mailboxId",
          "from"
        ],
        "properties": {
          "mailboxId": {
            "type": "string",
            "format": "uuid"
          },
          "externalId": {
            "type": "string"
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "subject": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "html": {
            "type": "string"
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "parse": {
            "type": "boolean"
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AttachmentUpload"
            }
          }
        }
      },
      "AttachmentUpload": {
        "type": "object",
        "properties": {
          "filename": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "contentBase64": {
            "type": "string"
          }
        }
      },
      "Attachment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "filename": {
            "type": "string"
          },
          "download_url": {
            "type": "string"
          },
          "body": {
            "type": "object",
            "properties": {
              "available": {
                "type": "boolean"
              },
              "href": {
                "type": "string"
              }
            }
          }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "mailboxId": {
            "type": "string",
            "format": "uuid"
          },
          "fromAddress": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "textBody": {
            "type": "string"
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "digest_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Attachment"
            }
          },
          "body": {
            "type": "object"
          },
          "deleted": {
            "type": "boolean"
          }
        }
      },
      "Tombstone": {
        "type": "object",
        "properties": {
          "message_id": {
            "type": "string",
            "format": "uuid"
          },
          "mailbox_id": {
            "type": "string",
            "format": "uuid"
          },
          "reason": {
            "type": "string"
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Digest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "window": {
            "enum": [
              "today",
              "yesterday",
              "last_7_days",
              "month"
            ]
          },
          "summaryMarkdown": {
            "type": "string"
          },
          "message_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        }
      },
      "DomainSetupRequest": {
        "type": "object",
        "required": [
          "domain"
        ],
        "properties": {
          "domain": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "purchase": {
            "type": "boolean"
          },
          "buy": {
            "type": "boolean"
          },
          "purchaseConsent": {
            "type": "boolean"
          },
          "autoRenew": {
            "type": "boolean",
            "default": false
          },
          "quoteToken": {
            "type": "string",
            "description": "Required when purchase/buy is true. Signed token returned by GET /api/v1/domains/availability."
          },
          "maxPriceCents": {
            "type": "integer",
            "minimum": 1,
            "description": "Required when purchase/buy is true. Maximum all-in price authorized by the caller."
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Za-z]{3}$",
            "description": "Required when purchase/buy is true. Three-letter authorized quote currency."
          },
          "catchAll": {
            "type": "boolean"
          },
          "inbound": {
            "type": "boolean"
          },
          "mxMigrationConsent": {
            "type": "boolean"
          },
          "dnsDelegationConsent": {
            "type": "boolean"
          }
        }
      },
      "DomainPurchaseRequest": {
        "type": "object",
        "required": [
          "domain",
          "quoteToken",
          "maxPriceCents",
          "currency"
        ],
        "properties": {
          "domain": {
            "type": "string"
          },
          "address": {
            "type": "string",
            "description": "Mailbox local part to create or plan."
          },
          "inbound": {
            "type": "boolean"
          },
          "mxMigrationConsent": {
            "type": "boolean"
          },
          "dnsDelegationConsent": {
            "type": "boolean"
          },
          "autoRenew": {
            "type": "boolean",
            "default": false,
            "description": "Domain auto-renew is off by default; opt in explicitly."
          },
          "quoteToken": {
            "type": "string",
            "description": "Signed quote token returned by GET /api/v1/domains/availability."
          },
          "maxPriceCents": {
            "type": "integer",
            "minimum": 1,
            "description": "Maximum all-in price authorized by the caller in minor units."
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Za-z]{3}$",
            "description": "Three-letter currency authorized by the caller."
          }
        }
      },
      "DomainAvailability": {
        "type": "object",
        "required": [
          "domain",
          "available",
          "provider",
          "mode",
          "purchase_supported"
        ],
        "properties": {
          "domain": {
            "type": "string"
          },
          "available": {
            "type": "boolean"
          },
          "premium": {
            "type": "boolean"
          },
          "provider": {
            "type": "string"
          },
          "resolved_provider": {
            "type": "string"
          },
          "mode": {
            "enum": [
              "test",
              "live"
            ]
          },
          "purchase_supported": {
            "type": "boolean"
          },
          "purchase_quote": {
            "$ref": "#/components/schemas/DomainPurchaseQuote"
          }
        }
      },
      "DomainLifecycleStep": {
        "type": "object",
        "required": [
          "step",
          "label",
          "status",
          "readiness"
        ],
        "properties": {
          "step": {
            "enum": [
              "register",
              "dns",
              "verify",
              "dmarc",
              "inbound"
            ]
          },
          "label": {
            "type": "string"
          },
          "status": {
            "enum": [
              "completed",
              "in_progress",
              "pending",
              "blocked",
              "failed",
              "skipped"
            ]
          },
          "readiness": {
            "type": "string"
          }
        }
      },
      "DomainPurchaseResult": {
        "type": "object",
        "description": "Result of POST /api/v1/domains/purchase. Extends the tenant-domain status envelope with per-step lifecycle progress.",
        "required": [
          "id",
          "domain",
          "lifecycle_steps",
          "ownership_ready",
          "resumed"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "domain": {
            "type": "string"
          },
          "ownership_ready": {
            "type": "boolean",
            "description": "True once ownershipStatus is ready (the PIVOT P5 acceptance)."
          },
          "resumed": {
            "type": "boolean",
            "description": "True when the domain was already registered and this call resumed the lifecycle instead of charging/registering again."
          },
          "verify_error": {
            "type": "string",
            "description": "Present when the verify step failed; the registration is preserved. The same Idempotency-Key exactly replays this stored result; obtain a fresh quote and use a new key to continue the incomplete lifecycle."
          },
          "lifecycle_steps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DomainLifecycleStep"
            }
          },
          "provider_status": {
            "$ref": "#/components/schemas/DomainProviderStatus"
          }
        }
      },
      "DomainProviderStatus": {
        "type": "object",
        "required": [
          "domain",
          "mode",
          "status",
          "records"
        ],
        "properties": {
          "domain": {
            "type": "string"
          },
          "mode": {
            "enum": [
              "test",
              "live"
            ]
          },
          "status": {
            "type": "string"
          },
          "purchase": {
            "type": "boolean"
          },
          "purchase_status": {
            "type": "string"
          },
          "dkim_status": {
            "type": "string"
          },
          "verified_for_sending": {
            "type": "boolean"
          },
          "records": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TenantDomainDnsRecord"
            }
          },
          "delegation_status": {
            "type": "string"
          },
          "domain_record": {
            "$ref": "#/components/schemas/TenantDomain"
          }
        }
      },
      "ProviderInboundWebhookResult": {
        "type": "object",
        "required": [
          "received",
          "imported",
          "unmatched",
          "blocked"
        ],
        "properties": {
          "received": {
            "const": true
          },
          "sesMessageId": {
            "type": "string"
          },
          "imported": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "tenantId",
                "mailboxId",
                "tenantDomainId",
                "domain",
                "messageId",
                "recipient"
              ],
              "properties": {
                "tenantId": {
                  "type": "string",
                  "format": "uuid"
                },
                "mailboxId": {
                  "type": "string",
                  "format": "uuid"
                },
                "tenantDomainId": {
                  "type": "string",
                  "format": "uuid"
                },
                "domain": {
                  "type": "string"
                },
                "messageId": {
                  "type": "string",
                  "format": "uuid"
                },
                "recipient": {
                  "type": "string",
                  "format": "email"
                }
              }
            }
          },
          "unmatched": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            }
          },
          "blocked": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "recipient",
                "domain",
                "code",
                "missing"
              ],
              "properties": {
                "recipient": {
                  "type": "string",
                  "format": "email"
                },
                "domain": {
                  "type": "string"
                },
                "code": {
                  "enum": [
                    "recipient_domain_system_reserved",
                    "inbound_domain_not_configured",
                    "inbound_domain_not_ready",
                    "inbound_domain_ambiguous"
                  ]
                },
                "missing": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "tenantId": {
                  "type": "string",
                  "format": "uuid"
                },
                "mailboxId": {
                  "type": "string",
                  "format": "uuid"
                },
                "tenantDomainId": {
                  "type": "string",
                  "format": "uuid"
                },
                "readiness": {
                  "type": "object"
                },
                "enabled": {
                  "type": "object"
                },
                "status": {
                  "type": "string"
                },
                "restriction_status": {
                  "type": "string"
                },
                "candidates": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  }
                }
              }
            }
          }
        }
      },
      "ProviderEventsWebhookResult": {
        "type": "object",
        "required": [
          "received"
        ],
        "properties": {
          "received": {
            "const": true
          },
          "ignored": {
            "type": "boolean"
          },
          "reason": {
            "type": "string"
          },
          "eventType": {
            "type": "string"
          },
          "normalizedEventType": {
            "enum": [
              "send",
              "delivery",
              "bounce",
              "complaint",
              "reject",
              "rendering_failure",
              "unknown"
            ]
          },
          "matched": {
            "type": "integer"
          },
          "recipients": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            }
          },
          "suppressed": {
            "type": "integer"
          },
          "domainEvents": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "tenantDomainId",
                "domain",
                "event",
                "restricted"
              ],
              "properties": {
                "tenantDomainId": {
                  "type": "string",
                  "format": "uuid"
                },
                "domain": {
                  "type": "string"
                },
                "event": {
                  "type": "string"
                },
                "restricted": {
                  "type": "boolean"
                }
              }
            }
          },
          "restrictedDomains": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        }
      },
      "CheckoutRequest": {
        "type": "object",
        "properties": {
          "kind": {
            "enum": [
              "credit_pack",
              "subscription"
            ]
          },
          "credits": {
            "type": "integer"
          },
          "plan": {
            "enum": [
              "pro",
              "scale"
            ],
            "description": "Paid identity tier to subscribe to (defaults to 'pro'). The 'free' tier has no checkout."
          }
        }
      },
      "AccountDeleteRequest": {
        "type": "object",
        "required": [
          "password"
        ],
        "properties": {
          "password": {
            "type": "string",
            "description": "The owner's current password. Password re-authentication is mandatory; a bearer token alone cannot delete the account."
          }
        }
      },
      "AccountExport": {
        "type": "object",
        "description": "GDPR Art. 20 data export. The small collections (tenant, users, mailboxes, labels, api_keys, domains, subscriptions, credits) are complete on every page; messages (with full bodies and attachment metadata) are keyset-paginated on id ASC — walk messages.next_cursor until null and concatenate messages.data.",
        "required": [
          "export_version",
          "generated_at",
          "tenant",
          "users",
          "mailboxes",
          "labels",
          "domains",
          "subscriptions",
          "credits",
          "messages"
        ],
        "properties": {
          "export_version": {
            "type": "integer",
            "const": 1
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "tenant": {
            "$ref": "#/components/schemas/Tenant"
          },
          "users": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/User"
            },
            "description": "Tenant users without password hashes."
          },
          "mailboxes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Mailbox"
            }
          },
          "labels": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Label"
            }
          },
          "api_keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKeyRecord"
            },
            "description": "API key metadata only; secrets are never exported."
          },
          "domains": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TenantDomain"
            }
          },
          "subscriptions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "credits": {
            "type": "object",
            "properties": {
              "balance": {
                "type": "integer"
              },
              "monthly_grant": {
                "type": "integer"
              },
              "ledger": {
                "type": "array",
                "items": {
                  "type": "object"
                },
                "description": "Complete credit transaction history, oldest first."
              }
            }
          },
          "messages": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "type": "object",
                  "description": "Full message rows including rawEmail, textBody, htmlBody, cleanMarkdown, classification, and metadata, plus attachment metadata with a download_url per attachment (binary content stays behind GET /api/v1/attachments/{id}/download)."
                }
              },
              "next_cursor": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Message-id keyset cursor; null on the last page."
              },
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      },
      "DomainPurchaseQuote": {
        "type": "object",
        "required": [
          "token",
          "domain",
          "quoted_price_cents",
          "currency",
          "expires_at",
          "idempotency_key_required"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "Signed, short-lived quote token bound to tenant, domain, customer total, currency, and expiry."
          },
          "domain": {
            "type": "string"
          },
          "quoted_price_cents": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "idempotency_key_required": {
            "type": "boolean",
            "const": true
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid bearer token",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Token lacks the required scope or session type",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Requested resource was not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Request was rate limited; Retry-After may be present",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BillingRequired": {
        "description": "Paid-surface gate (the custom-domain routes): the tenant needs an active subscription or a positive credit balance backed by a qualifying grant (details.required is active_subscription_or_credits; details.hint names the upgrade command). The free-tier surface — key minting, the shared-domain mailbox, metered sending, message operations — never returns this for a verified tenant. When the newest subscription is past_due, details.past_due_recovery names the two recovery paths: fix_payment via POST /api/v1/billing/portal, or spend_credits (cancel the subscription in the portal and keep spending the remaining qualifying credit balance).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SubscriptionExists": {
        "description": "Tenant already has an active, trialing, or past_due subscription; manage it through POST /api/v1/billing/portal instead of subscribing again",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "Public"
        ],
        "security": [],
        "summary": "Health, deploy metadata, and a short-deadline DB ping",
        "responses": {
          "200": {
            "description": "Healthy; db.ok is true",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthInfo"
                }
              }
            }
          },
          "503": {
            "description": "Degraded; the database ping failed or timed out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthInfo"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1": {
      "get": {
        "tags": [
          "Public"
        ],
        "security": [],
        "summary": "API metadata",
        "responses": {
          "200": {
            "description": "Platform metadata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlatformInfo"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "Public"
        ],
        "security": [],
        "summary": "OpenAPI document",
        "responses": {
          "200": {
            "description": "OpenAPI JSON"
          }
        }
      }
    },
    "/docs": {
      "get": {
        "tags": [
          "Public"
        ],
        "security": [],
        "summary": "Human API docs",
        "responses": {
          "200": {
            "description": "HTML or Markdown API docs"
          }
        }
      }
    },
    "/legal/terms": {
      "get": {
        "tags": [
          "Public"
        ],
        "security": [],
        "summary": "Terms of Service (HTML)",
        "responses": {
          "200": {
            "description": "Terms of Service document",
            "content": {
              "text/html": {}
            }
          }
        }
      }
    },
    "/legal/privacy": {
      "get": {
        "tags": [
          "Public"
        ],
        "security": [],
        "summary": "Privacy Policy (HTML)",
        "responses": {
          "200": {
            "description": "Privacy Policy document",
            "content": {
              "text/html": {}
            }
          }
        }
      }
    },
    "/legal/refunds": {
      "get": {
        "tags": [
          "Public"
        ],
        "security": [],
        "summary": "Refund Policy (HTML)",
        "responses": {
          "200": {
            "description": "Refund Policy document",
            "content": {
              "text/html": {}
            }
          }
        }
      }
    },
    "/changelog": {
      "get": {
        "tags": [
          "Public"
        ],
        "security": [],
        "summary": "API contract changelog (also at /docs/changelog and /api/v1/changelog)",
        "responses": {
          "200": {
            "description": "Markdown changelog of API contract changes"
          }
        }
      }
    },
    "/api/v1/auth/signup": {
      "post": {
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Create a user account",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignupRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session and account context. No API key secret is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthResult"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "description": "Signup rate limited"
          }
        }
      }
    },
    "/api/v1/auth/login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Create a session",
        "description": "Per-IP rate limit plus per-account exponential lockout: repeated failed logins against one email return 429 account_locked with a Retry-After header regardless of source IP. The lockout clears on successful login or password reset.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session and account context",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "description": "Rate limited or account locked (account_locked) with a Retry-After header"
          }
        }
      }
    },
    "/api/v1/auth/logout": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Revoke a session token",
        "responses": {
          "200": {
            "description": "Revoked"
          }
        }
      }
    },
    "/api/v1/auth/password/forgot": {
      "post": {
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Request a password reset email",
        "description": "Uniform 200 whether or not an account exists for the email (no enumeration). The 200 is returned immediately: token issuance and email delivery run asynchronously after the response (failures are logged server-side, never surfaced), so neither the body nor response timing distinguishes real accounts from unknown emails. Rate-limited per IP and per email. Real accounts receive a single-use reset link that expires after 30 minutes; only a SHA-256 hash of the token is stored.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted. The response is identical whether or not the account exists."
          },
          "429": {
            "description": "Rate limited (per IP or per email)"
          }
        }
      }
    },
    "/api/v1/auth/password/reset": {
      "post": {
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Set a new password from a reset token",
        "description": "Consumes a single-use reset token, enforces the signup password policy, revokes every live session for the account, and clears any login lockout.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token",
                  "password"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string",
                    "minLength": 8
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password updated; all of the user's sessions are revoked"
          },
          "400": {
            "description": "Invalid, expired, or already-used token (invalid_token) or password policy violation (bad_request)"
          },
          "429": {
            "description": "Rate limited"
          }
        }
      }
    },
    "/api/v1/auth/verify": {
      "post": {
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Verify an email address from a verification token",
        "description": "Consumes a single-use verification token (24-hour expiry) and marks the user's email verified.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Email verified"
          },
          "400": {
            "description": "Invalid, expired, or already-used token (invalid_token)"
          },
          "429": {
            "description": "Rate limited"
          }
        }
      }
    },
    "/api/v1/auth/verify/resend": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Resend the email verification link",
        "description": "Requires an interactive user session. Rate-limited per user and per IP. Returns verified: true without sending when the email is already verified.",
        "responses": {
          "200": {
            "description": "Verification email sent, or already verified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "description": "Rate limited"
          }
        }
      }
    },
    "/api/v1/auth/providers": {
      "get": {
        "tags": [
          "Auth"
        ],
        "security": [],
        "summary": "Available auth providers",
        "responses": {
          "200": {
            "description": "Provider flags and capabilities (password_reset, email_verification, email_verification_required)"
          }
        }
      }
    },
    "/api/v1/auth/me": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Current user and tenant",
        "responses": {
          "200": {
            "description": "Current auth context including user.emailVerifiedAt"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/account/export": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Export all tenant data as JSON (GDPR Art. 20). Owner/admin user session required; never billing-gated.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Messages per page, 1-1000 (default 200). The non-message collections are complete on every page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Message id from a prior messages.next_cursor. Anything else is 400.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tenant data export page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountExport"
                }
              }
            }
          },
          "400": {
            "description": "Invalid cursor"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "description": "Export rate limited (600 requests per tenant per hour); resume from the same cursor in the next window"
          }
        }
      }
    },
    "/api/v1/account": {
      "delete": {
        "tags": [
          "Account"
        ],
        "summary": "Permanently delete the account and all tenant data (GDPR Art. 17). Owner session + password re-auth required.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountDeleteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tenant deleted; all sessions and API keys are revoked, every tenant-owned row is removed via cascade, and object storage-stored attachment binaries are removed best-effort after the cascade commits. An account.delete audit row survives with the tenant reference severed."
          },
          "400": {
            "description": "Password missing from the body"
          },
          "403": {
            "description": "Password re-authentication failed, or the session is not the workspace owner",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A non-canceled subscription exists (the check re-runs inside the deleting transaction, so a concurrently created subscription also aborts the delete); cancel it in the Stripe customer portal (POST /api/v1/billing/portal) first. Details list the blocking subscriptions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Deletion attempts rate limited"
          }
        }
      }
    },
    "/api/v1/api-keys": {
      "get": {
        "tags": [
          "API Keys"
        ],
        "summary": "List API keys",
        "responses": {
          "200": {
            "description": "API key records"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "API Keys"
        ],
        "summary": "Create an API key",
        "description": "Requires an interactive owner/admin user session. When MAILERY_REQUIRE_EMAIL_VERIFICATION is enabled (default in production), the verified-email requirement is enforced directly at this route, so a key can only be minted by a verified owner/admin session (403 email_verification_required otherwise). Available on the Free plan (PIVOT D-4): a verified tenant mints keys without any subscription or credit balance.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "enum": [
                        "full",
                        "mail_read",
                        "mail_write",
                        "billing_read",
                        "domains:purchase"
                      ]
                    },
                    "description": "Omit for the least-privilege default ['mail_read','mail_write']. 'domains:purchase' must be requested explicitly."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "One-time API key secret and record"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/keys/revoke-all": {
      "post": {
        "tags": [
          "API Keys"
        ],
        "summary": "Break-glass: revoke ALL API keys",
        "description": "Kill-switch that revokes every live API key for the tenant. Authenticates itself so it works with zero usable keys: send a valid session bearer token, OR send { email, password } in the body. The password path shares the per-account login lockout and is rate-limited per IP. Returns { ok, revoked, via }.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Required when not using a session bearer token."
                  },
                  "password": {
                    "type": "string",
                    "description": "Required when not using a session bearer token."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok, revoked, via }"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/api-keys/{id}/revoke": {
      "post": {
        "tags": [
          "API Keys"
        ],
        "summary": "Revoke an API key",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/feedback": {
      "get": {
        "tags": [
          "Feedback"
        ],
        "summary": "List the caller's own feedback",
        "description": "Returns the authenticated tenant actor's own feedback, newest first. Tenant-scoped and narrowed to the requesting actor. Not billing-gated.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size 1-200 (default 50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's feedback entries"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Feedback"
        ],
        "summary": "Send product feedback",
        "description": "Persists tenant-scoped product feedback via the @hasna/feedback SDK store. Any authenticated tenant actor (user session or API key) may submit — deliberately NOT billing-gated. Rate-limited to 30 per tenant and 60 per client IP per hour. The optional screenshot is a best-effort client PNG data URL (decoded, 6 MB size-cap, stored by reference — never embedded in the row) that never blocks the text.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "message": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "bug",
                      "idea",
                      "question",
                      "praise",
                      "other"
                    ]
                  },
                  "severity": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high",
                      "critical"
                    ]
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Absolute http(s) URL of the page the feedback is about. Relative URLs are rejected with 400."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "context": {
                    "type": "object",
                    "properties": {
                      "route": {
                        "type": "string"
                      },
                      "url": {
                        "type": "string",
                        "format": "uri",
                        "description": "Absolute http(s) URL. Relative URLs are rejected with 400."
                      }
                    }
                  },
                  "screenshot": {
                    "type": "string",
                    "description": "Optional client-captured PNG data URL (data:image/png;base64,…). Best-effort."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created feedback entry"
          },
          "400": {
            "description": "Missing/oversized message, or an unknown kind/severity"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "description": "Feedback rate limited; retry after the Retry-After header"
          }
        }
      }
    },
    "/api/v1/mcp": {
      "post": {
        "tags": [
          "MCP"
        ],
        "summary": "MCP (Model Context Protocol) HTTP transport",
        "description": "JSON-RPC 2.0 endpoint for the MCP interface. Accepts a single request, builds an in-process MaileryClient with the bearer token, and dispatches to the shared MCP core. Returns 200 with JSON-RPC response, 202 for notifications (no id), or 400 for parse errors. Requires authentication and respects scopes/entitlements. Rate-limited to 100 requests per tenant per minute. Admin/operator tools are not exposed; only the eight agent-facing SDK tools are available (send_email, list_inbox, read_message, search_domains, buy_domain, create_mailbox, get_billing_status, submit_feedback).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "jsonrpc": {
                    "type": "string",
                    "enum": [
                      "2.0"
                    ]
                  },
                  "id": {
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "number"
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "description": "Request identifier. Omit for notifications."
                  },
                  "method": {
                    "type": "string",
                    "description": "JSON-RPC method: initialize, tools/list, tools/call, ping, notifications/initialized, notifications/cancelled"
                  },
                  "params": {
                    "type": "object",
                    "description": "Optional method parameters"
                  }
                }
              },
              "examples": {
                "tools_list": {
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "tools/list"
                  }
                },
                "tools_call": {
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 2,
                    "method": "tools/call",
                    "params": {
                      "name": "get_billing_status",
                      "arguments": {}
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response (for requests with an id)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "type": "string",
                      "enum": [
                        "2.0"
                      ]
                    },
                    "id": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "result": {
                      "type": "object"
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "integer"
                        },
                        "message": {
                          "type": "string"
                        },
                        "data": {}
                      }
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Accepted (for notifications without an id)"
          },
          "400": {
            "description": "Invalid JSON or JSON-RPC parse error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "description": "Rate limited (100 per tenant per minute)"
          }
        }
      }
    },
    "/api/v1/billing/overview": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "Billing and credits overview",
        "responses": {
          "200": {
            "description": "Balance, plans, packs, subscriptions, and ledger"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/billing/checkout": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "Create a Stripe checkout URL",
        "description": "Creates a Stripe checkout session for a Pro/Scale subscription or a one-time credit pack. Requires an interactive owner/admin session. Rate limited to 10 attempts per tenant/actor per hour. Purchased credits never expire. When MAILERY_REQUIRE_EMAIL_VERIFICATION is enabled (default in production), an unverified email is rejected with 403 email_verification_required.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout URL"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/SubscriptionExists"
          },
          "429": {
            "description": "Checkout rate limited; retry after the Retry-After header"
          }
        }
      }
    },
    "/api/v1/billing/portal": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "Create a Stripe portal URL",
        "responses": {
          "200": {
            "description": "Portal URL"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/mailboxes": {
      "get": {
        "tags": [
          "Mailboxes"
        ],
        "summary": "List mailboxes",
        "responses": {
          "200": {
            "description": "Mailbox list"
          }
        }
      },
      "post": {
        "tags": [
          "Mailboxes"
        ],
        "summary": "Create or connect a mailbox (plan quotas apply)",
        "description": "Plan mailbox-count caps are enforced race-safely at creation. Free-plan tenants get exactly one mailbox and it must be on the shared subdomain (MAILERY_SHARED_DOMAIN); shared-domain addresses are unique across ALL tenants and always use the hosted provider (hosted; sandbox for test rigs).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Mailbox"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Mailbox"
          },
          "402": {
            "description": "Plan quota or free-plan placement: mailbox_quota_exceeded, or free_plan_shared_domain_required (free-plan mailbox off the shared subdomain)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "mailbox_address_taken: the shared-domain address is already owned by another tenant",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "shared_domain_not_configured: free-tier mailboxes need MAILERY_SHARED_DOMAIN configured on the server",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages": {
      "get": {
        "tags": [
          "Messages"
        ],
        "summary": "List/search messages (bodyless keyset projection with hybrid full-text search)",
        "parameters": [
          {
            "name": "mailboxId",
            "in": "query",
            "description": "Scope to one mailbox (UUID). Alias: mailbox_id. Non-UUID is 400.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "folder",
            "in": "query",
            "description": "Folder filter. Alias: group; 'archived' aliases 'archive'. 'starred' lists the star flag; 'sent' lists outbound. Unknown value is 400 (never silently returns spam/trash).",
            "schema": {
              "type": "string",
              "enum": [
                "inbox",
                "important",
                "unread",
                "spam",
                "trash",
                "archive",
                "archived",
                "starred",
                "sent"
              ]
            }
          },
          {
            "name": "direction",
            "in": "query",
            "description": "Message direction filter. Unknown value is 400.",
            "schema": {
              "type": "string",
              "enum": [
                "inbound",
                "outbound"
              ]
            }
          },
          {
            "name": "threadId",
            "in": "query",
            "description": "List one thread (RFC 5322 thread key). Alias: thread_id. A blank/whitespace value is 400 (never silently lists the whole mailbox).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "domain",
            "in": "query",
            "description": "Repeatable mailbox-scope domain filter (shared self-hosted/cloud contract): only messages in the tenant's mailboxes whose ADDRESS is at one of these domains. Case-insensitive; a leading '@' is tolerated; IDN domains match under either spelling (unicode or punycode). Repeat the parameter for multiple domains (max 16 distinct). A blank/whitespace value collapses to no filter (like folder=); a malformed value is 400. Cannot be combined with updatedSince (400).",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "ISO timestamp. Transparently routes this list to the delta feed (created-or-changed since the watermark, ordered by updated_at ASC) — see GET /messages/changes. Alias: updated_since. Cannot be combined with q/group/direction/threadId (that is a 400).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Hybrid search: GIN tsvector whole-word/ranked matching plus pg_trgm substring and partial-email matching over subject, from_address, snippet, and recipients (to/cc). LIKE metacharacters are literal.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size 1-200 (default 50).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque (sort_at,id) keyset cursor from a prior next_cursor. Absent = page 1; an explicitly-provided undecodable/old-version cursor is 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bodyless message page with a full-microsecond keyset next_cursor",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageListPage"
                }
              }
            }
          },
          "400": {
            "description": "Unknown folder/direction, non-UUID mailboxId, blank threadId, updatedSince combined with q/group/direction/threadId, or an undecodable/old-version cursor"
          }
        }
      },
      "post": {
        "tags": [
          "Messages"
        ],
        "summary": "Upload a hosted message",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessageUpload"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Message"
          },
          "402": {
            "description": "insufficient_credits: upload with parse: true spends the parse credit cost",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages/groups": {
      "get": {
        "tags": [
          "Messages"
        ],
        "summary": "Message group counts",
        "parameters": [
          {
            "name": "mailboxId",
            "in": "query",
            "description": "Per-mailbox rollup (UUID). Alias: mailbox_id. Omit for the tenant-wide rollup.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "domain",
            "in": "query",
            "description": "Repeatable mailbox-scope domain filter (shared self-hosted/cloud contract): counts scoped to the tenant's mailboxes whose ADDRESS is at one of these domains. Case-insensitive; leading '@' tolerated; IDN domains match under either spelling; max 16 distinct; blank collapses to no filter; malformed value is 400.",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Group counts (total, inbox, unread, important, spam, trash, archive, starred, sent)."
          }
        }
      }
    },
    "/api/v1/labels": {
      "get": {
        "tags": [
          "Messages"
        ],
        "summary": "List tenant labels with per-label live-message counts",
        "description": "Tenant-scoped label set with a per-label count so a client renders a labels sidebar without deriving labels from a sampled page. System-label counts read the folder counters (message_counters); custom-label counts are computed from message_labels.",
        "responses": {
          "200": {
            "description": "Labels with counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Label"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages/changes": {
      "get": {
        "tags": [
          "Messages"
        ],
        "summary": "Delta / changesSince feed (created-or-changed since a watermark)",
        "description": "Live messages created OR changed since a watermark, ordered by updated_at ASC and keyset-paginated. Each row carries its exact updated_at; deletions come from GET /messages/tombstones. Together they let a bounded-cache client sync without re-fetching pages. Returns a server-time watermark (X-Mailery-Server-Time header + server_time/state in the body).",
        "parameters": [
          {
            "name": "updatedSince",
            "in": "query",
            "description": "ISO timestamp watermark (inclusive/gte). Aliases: updated_since, since.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "mailboxId",
            "in": "query",
            "description": "Scope to one mailbox (UUID). Alias: mailbox_id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size 1-500 (default 100).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque (updated_at,id) keyset cursor from a prior next_cursor. An undecodable/old-version cursor is 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delta page with a full-microsecond (updated_at,id) keyset next_cursor and a server-time watermark",
            "headers": {
              "X-Mailery-Server-Time": {
                "schema": {
                  "type": "string",
                  "format": "date-time"
                },
                "description": "Server clock captured before the read; adopt as the next updatedSince once fully drained."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageChangesPage"
                }
              }
            }
          },
          "400": {
            "description": "Non-UUID mailboxId or an undecodable/old-version cursor"
          }
        }
      }
    },
    "/api/v1/messages/bulk": {
      "post": {
        "tags": [
          "Messages"
        ],
        "summary": "Bounded, counter-correct bulk message mutation",
        "description": "Marks read/unread, stars, archives, spams, trashes, adds/removes a label, or deletes a bounded set of messages selected by an explicit id list OR a { mailboxId, folder } filter. Tenant-scoped and counter-correct; large folders drain in keyset batches with a resume cursor.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkMessageRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Counts affected/matched plus a resume cursor",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkMessageResult"
                }
              }
            }
          },
          "400": {
            "description": "Unknown action, > 1000 explicit ids, no target selector, or an undecodable resume cursor"
          }
        }
      }
    },
    "/api/v1/messages/send": {
      "post": {
        "tags": [
          "Messages"
        ],
        "summary": "Send hosted mail from an outbound-ready tenant domain or the shared subdomain",
        "description": "Sends from a tenant-owned domain require the full domain-readiness gates and spend credits. Sends from the shared subdomain (MAILERY_SHARED_DOMAIN) skip the per-tenant domain gates; free-plan tenants are metered against the plan's monthly allowance (3,000 sends per UTC calendar month, no rollover), then any purchased credit balance.",
        "responses": {
          "202": {
            "description": "Queued/sent message"
          },
          "402": {
            "description": "send_limit_reached (free monthly allowance and credit balance exhausted; details.hint names the upgrade command) or insufficient_credits (paid plan with a drained balance)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Sender domain is missing ownership, DNS, outbound, enablement, billing, or restriction gates"
          }
        }
      }
    },
    "/api/v1/messages/tombstones": {
      "get": {
        "tags": [
          "Messages"
        ],
        "summary": "List deleted-message tombstones",
        "description": "Deletions feed for changesSince sync. Tenant-wide by default; an optional mailboxId scopes it to one mailbox's deletions.",
        "parameters": [
          {
            "name": "mailboxId",
            "in": "query",
            "description": "Scope to one mailbox's deletions (UUID). Alias: mailbox_id. Non-UUID is 400. Omit for the tenant-wide feed.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "ISO timestamp; only tombstones deleted at or after this instant.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size 1-500 (default 100).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tombstones",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Tombstone"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Non-UUID mailboxId or a non-ISO since"
          }
        }
      }
    },
    "/api/v1/messages/{id}": {
      "get": {
        "tags": [
          "Messages"
        ],
        "summary": "Read a message",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Message"
          }
        }
      },
      "patch": {
        "tags": [
          "Messages"
        ],
        "summary": "Update message flags",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Any subset of the boolean flags. Only booleans are applied; the change bumps updated_at so the delta feed observes it.",
                "properties": {
                  "isRead": {
                    "type": "boolean"
                  },
                  "isImportant": {
                    "type": "boolean"
                  },
                  "isArchived": {
                    "type": "boolean"
                  },
                  "isSpam": {
                    "type": "boolean"
                  },
                  "isTrash": {
                    "type": "boolean"
                  },
                  "isStarred": {
                    "type": "boolean",
                    "description": "Real star flag, distinct from isImportant."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated message"
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Messages"
        ],
        "summary": "Delete a message",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tombstone"
          }
        }
      }
    },
    "/api/v1/messages/{id}/labels": {
      "post": {
        "tags": [
          "Messages"
        ],
        "summary": "Add a label to a message",
        "description": "Adds a custom or system label. Creating the link bumps the message's updated_at so the delta feed observes it. Returns the message's full label set.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string",
                    "description": "Alias of label."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated label set",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "label_records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Label"
                      }
                    },
                    "label_names": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Empty/whitespace label"
          },
          "404": {
            "description": "Message not found (tenant-scoped)"
          }
        }
      }
    },
    "/api/v1/messages/{id}/labels/{label}": {
      "delete": {
        "tags": [
          "Messages"
        ],
        "summary": "Remove a label from a message",
        "description": "Idempotent. Removing an existing link bumps updated_at; removing an absent label is a no-op. Returns the message's remaining label set.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "label",
            "in": "path",
            "required": true,
            "description": "URL-encoded label name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Remaining label set",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "label_records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Label"
                      }
                    },
                    "label_names": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Message not found (tenant-scoped)"
          }
        }
      }
    },
    "/api/v1/messages/{id}/parse": {
      "post": {
        "tags": [
          "Messages"
        ],
        "summary": "Parse/classify a message",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Parsed message data"
          },
          "402": {
            "description": "insufficient_credits: the parse credit cost exceeds the tenant's balance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages/{id}/attachments": {
      "get": {
        "tags": [
          "Messages"
        ],
        "summary": "List message attachments",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attachment list"
          }
        }
      }
    },
    "/api/v1/attachments/{id}/download": {
      "get": {
        "tags": [
          "Messages"
        ],
        "summary": "Download an attachment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attachment bytes"
          }
        }
      }
    },
    "/api/v1/digests": {
      "get": {
        "tags": [
          "Digests"
        ],
        "summary": "List digests",
        "responses": {
          "200": {
            "description": "Digest list"
          }
        }
      }
    },
    "/api/v1/digests/generate": {
      "post": {
        "tags": [
          "Digests"
        ],
        "summary": "Generate a digest",
        "description": "Spends MAILERY_COST_DIGEST credits with an idempotent charge per (tenant, window, UTC day): a same-day repeat for the same window returns the stored digest without re-running the model or re-billing. Rate-limited per tenant (60/hour).",
        "responses": {
          "200": {
            "description": "Digest"
          },
          "402": {
            "description": "insufficient_credits: the digest credit cost exceeds the tenant's balance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited: more than 60 digest generations per tenant per hour",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/domains": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "List tenant domains",
        "responses": {
          "200": {
            "description": "Tenant domain lifecycle records"
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          }
        }
      },
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Add a tenant domain lifecycle record",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DomainSetupRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Tenant domain record"
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          },
          "409": {
            "description": "system_domain_reserved: the Mailery system domain cannot be a tenant domain; shared_domain_reserved: the shared free-tier sending domain (and any subdomain of it) is platform-owned and cannot be configured as a tenant domain"
          }
        }
      }
    },
    "/api/v1/domains/availability": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "Check domain availability",
        "description": "Returns whether the domain can be used for a Mailery identity. Internal cost components and fees are not part of this public contract.",
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Availability and ownership status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainAvailability"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          }
        }
      }
    },
    "/api/v1/domains/setup": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Prepare a custom-domain identity",
        "description": "Connect or prepare a custom-domain identity and return the DNS records Mailery needs to verify it. When purchase/buy is true, the request requires a signed availability quote and Idempotency-Key and is serialized before paid domain setup.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DomainSetupRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Domain setup result (with a 'billing' block when purchased)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainProviderStatus"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 200
            },
            "description": "Required when purchase/buy is true."
          }
        ]
      }
    },
    "/api/v1/domains/purchase": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Purchase and prepare a custom-domain identity",
        "description": "Creates a paid custom-domain identity and drives it toward ownershipStatus: ready in one call. The same Idempotency-Key exactly replays its stored result. If verification is still pending or the stored result is incomplete, obtain a fresh quote and use a new key to continue from the current lifecycle state without double-charging or creating duplicate ownership. Requires domains:purchase scope and applies account, plan, and anti-abuse gates. AutoRenew defaults to false. The response carries lifecycle_steps, ownership_ready, and resumed.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DomainPurchaseRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Domain purchase lifecycle result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainPurchaseResult"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 200
            },
            "description": "Caller-supplied idempotency key required for paid domain purchase."
          }
        ]
      }
    },
    "/api/v1/domains/status": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "Domain setup status",
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Domain registration, DNS, mail identity, and bounce-domain status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainProviderStatus"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          }
        },
        "description": "Return product-level domain readiness and DNS records for a custom-domain identity."
      }
    },
    "/api/v1/domains/{id}": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "Read a tenant domain lifecycle record",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tenant domain with DNS records"
          },
          "404": {
            "description": "Not found"
          }
        }
      },
      "delete": {
        "tags": [
          "Domains"
        ],
        "summary": "Soft-delete a tenant domain",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted tenant domain"
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          }
        }
      }
    },
    "/api/v1/domains/{id}/dns": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "List tenant domain DNS records",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "DNS record list"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/domains/{id}/events": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "List tenant domain mail-plane events",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Domain event list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TenantDomainEvent"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/domains/{id}/dmarc/report": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Record a tenant domain DMARC aggregate observation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DmarcReportRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated tenant domain DMARC monitoring state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantDomain"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/domains/{id}/dmarc/policy": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Recommend or apply tenant domain DMARC enforcement after clean reports",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DmarcPolicyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated tenant domain DMARC policy and DNS records",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TenantDomain"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          },
          "409": {
            "description": "DMARC enforcement attempted before clean alignment evidence"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/domains/{id}/verify": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Refresh tenant domain readiness",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Verified tenant domain status"
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          }
        }
      }
    },
    "/api/v1/domains/{id}/enable-inbound": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Enable inbound after inbound readiness is ready",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Inbound enabled"
          },
          "409": {
            "description": "Domain is not inbound-ready"
          }
        }
      }
    },
    "/api/v1/domains/{id}/enable-outbound": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Enable outbound after outbound readiness is ready",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Outbound enabled"
          },
          "409": {
            "description": "Domain is not outbound-ready"
          }
        }
      }
    },
    "/api/v1/domains/{id}/disable": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Disable a tenant domain",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Domain disabled and restricted"
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          }
        }
      }
    },
    "/webhooks/stripe": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "summary": "Stripe webhook",
        "description": "Signature-verified Stripe webhook. Handles checkout.session.completed, checkout.session.async_payment_succeeded (fulfillment matches the stored pre-tax amount against the session's amount_subtotal or amount_total, so Stripe Tax-inclusive totals still fulfill), checkout.session.async_payment_failed (declined async payments record the benign outcome checkout_failed; the failed status is reserved for crashed handlers), checkout.session.expired, invoice.paid, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted, charge.refunded (idempotent credit clawback, may drive the balance negative), and charge.dispute.created (tenant flagged; transient charge-lookup failures return 5xx so Stripe retries). The Stripe endpoint must be pinned to API version 2025-02-24.acacia.",
        "responses": {
          "200": {
            "description": "Received"
          },
          "400": {
            "description": "Invalid, tampered, stale, or missing Stripe-Signature (invalid_signature)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Transient handler failure — Stripe retries"
          },
          "503": {
            "description": "Webhook not configured or retryable"
          }
        }
      }
    },
    "/api/v1/admin/overview": {
      "get": {
        "tags": [
          "Admin"
        ],
        "security": [
          {
            "adminBearer": []
          },
          {
            "bearerAuth": []
          }
        ],
        "summary": "Platform overview",
        "responses": {
          "200": {
            "description": "Counts, recent credit events, and billing-flagged tenants (refunds/disputes)"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/admin/credits/adjust": {
      "post": {
        "tags": [
          "Admin"
        ],
        "security": [
          {
            "adminBearer": []
          },
          {
            "bearerAuth": []
          }
        ],
        "summary": "Adjust tenant credits",
        "responses": {
          "200": {
            "description": "Updated balance"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/admin/audit/purge": {
      "post": {
        "tags": [
          "Admin"
        ],
        "security": [
          {
            "adminBearer": []
          },
          {
            "bearerAuth": []
          }
        ],
        "summary": "Purge expired audit rows",
        "description": "Deletes admin_audit rows older than 24 months and records a fresh audit.purge row.",
        "responses": {
          "200": {
            "description": "Removed row count and retention window"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/admin/billing/anomalies": {
      "get": {
        "tags": [
          "Admin"
        ],
        "security": [
          {
            "adminBearer": []
          },
          {
            "bearerAuth": []
          }
        ],
        "summary": "Billing anomaly sweep",
        "description": "Runs the billing reconciliation sweep. Stripe subscription checks use only the server-side Stripe configuration; clients never send Stripe keys.",
        "responses": {
          "200": {
            "description": "Billing anomalies report"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Admin"
        ],
        "security": [
          {
            "adminBearer": []
          },
          {
            "bearerAuth": []
          }
        ],
        "summary": "Billing anomaly sweep",
        "description": "Runs the billing reconciliation sweep with a JSON body containing optional minutes, limit, subscriptionLimit, and predeploy fields. Stripe keys are never accepted from the client.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdminBillingAnomaliesRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Billing anomalies report"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/admin/tenants/{id}/freeze": {
      "post": {
        "tags": [
          "Admin"
        ],
        "security": [
          {
            "adminBearer": []
          },
          {
            "bearerAuth": []
          }
        ],
        "summary": "Freeze a tenant",
        "description": "Idempotently freezes one tenant, blocking sends, domain purchases, and renewal credit grants.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantFreezeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tenant suspension state"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/admin/tenants/{id}/unfreeze": {
      "post": {
        "tags": [
          "Admin"
        ],
        "security": [
          {
            "adminBearer": []
          },
          {
            "bearerAuth": []
          }
        ],
        "summary": "Unfreeze a tenant",
        "description": "Idempotently unfreezes one tenant. clearBillingFlag defaults to true; pass false to preserve the billing flag.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantUnfreezeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tenant suspension state"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  }
}
