openapi: 3.1.0
info:
  title: Mektup API
  version: "1.0.0"
  description: >
    Self-hosted, multi-domain email platform. Register a domain, provision
    mailboxes, send and receive real email, all account-scoped and enforced
    server-side (including at the database layer via Postgres Row-Level
    Security). See docs/API.md for prose documentation and worked examples,
    and docs/MCP.md for using this API through an MCP server as an AI agent.
  contact:
    email: kontakt@codenord.dk
servers:
  - url: https://api.usemektup.com
    description: Production
security:
  - ApiKeyAuth: []

paths:
  /v1/me:
    get:
      summary: Get the authenticated caller's identity
      operationId: getMe
      tags: [Account]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  accountId: { type: string, format: uuid }
                  role: { type: [string, "null"], example: owner }
                  authMethod: { type: string, enum: [clerk, api_key] }

  /v1/usage:
    get:
      summary: Current billing tier and usage against its limits
      operationId: getUsage
      tags: [Billing]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Usage" }

  /v1/api-keys:
    get:
      summary: List API keys on this account
      description: The full key is never shown again after creation - only a display prefix.
      operationId: listApiKeys
      tags: [ApiKeys]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items: { $ref: "#/components/schemas/ApiKeySummary" }
    post:
      summary: Create a new API key
      description: The full key is returned exactly once, in this response - save it immediately, it cannot be retrieved again later.
      operationId: createApiKey
      tags: [ApiKeys]
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreateApiKeyResult" }

  /v1/api-keys/{id}:
    delete:
      summary: Revoke an API key
      description: Immediate - anything still using this key (a script, an MCP server, a CI job) loses access right away. Cannot be undone.
      operationId: revokeApiKey
      tags: [ApiKeys]
      parameters:
        - $ref: "#/components/parameters/IdParam"
      responses:
        "204": { description: Revoked }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains:
    post:
      summary: Register a domain
      description: >
        Generates a real DKIM keypair and returns the exact DNS records to
        add. Never modifies DNS itself.
      operationId: createDomain
      tags: [Domains]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain: { type: string, example: example.com }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreateDomainResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "402": { $ref: "#/components/responses/LimitReached" }
    get:
      summary: List domains on this account
      operationId: listDomains
      tags: [Domains]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  domains:
                    type: array
                    items: { $ref: "#/components/schemas/Domain" }

  /v1/domains/{domain}:
    delete:
      summary: Delete a domain and everything under it
      description: Cascades to every mailbox and message. Does not delete already-delivered mail bodies from object storage.
      operationId: deleteDomain
      tags: [Domains]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
      responses:
        "204": { description: Deleted }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/records:
    get:
      summary: Re-fetch DNS records for an already-registered domain
      operationId: getDomainRecords
      tags: [Domains]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  records:
                    type: array
                    items: { $ref: "#/components/schemas/DnsRecord" }
                  recommendation: { $ref: "#/components/schemas/Recommendation" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/verify:
    post:
      summary: Re-check a domain's live DNS and flip verified if it matches
      operationId: verifyDomain
      tags: [Domains]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  verified: { type: boolean }
                  mx: { type: array, items: { type: string } }
                  error: { type: string, description: "DNS resolver error code, present only on lookup failure" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/mailboxes:
    post:
      summary: Create a mailbox on a registered domain
      operationId: createMailbox
      tags: [Mailboxes]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [localPart]
              properties:
                localPart: { type: string, example: hello }
                password: { type: string, minLength: 12, description: "Omit to auto-generate a strong password" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreateMailboxResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    get:
      summary: List mailboxes on a domain
      operationId: listMailboxes
      tags: [Mailboxes]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  mailboxes:
                    type: array
                    items: { $ref: "#/components/schemas/Mailbox" }

  /v1/domains/{domain}/mailboxes/{localPart}:
    delete:
      summary: Delete a mailbox
      description: Postfix stops accepting mail for it immediately. Does not delete already-delivered mail from the mail store.
      operationId: deleteMailbox
      tags: [Mailboxes]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      responses:
        "204": { description: Deleted }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/mailboxes/{localPart}/password:
    put:
      summary: Reset a mailbox's IMAP/SMTP-AUTH password
      operationId: resetMailboxPassword
      tags: [Mailboxes]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                password: { type: string, minLength: 12, description: "Omit to auto-generate" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  address: { type: string, format: email }
                  password: { type: string, description: "Shown once if auto-generated" }

  /v1/domains/{domain}/mailboxes/{localPart}/forwards:
    get:
      summary: List forwarding addresses for a mailbox
      operationId: listForwards
      tags: [Forwarding]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  forwards:
                    type: array
                    items: { $ref: "#/components/schemas/Forward" }
    post:
      summary: Add a forwarding address
      operationId: addForward
      tags: [Forwarding]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [forwardTo]
              properties:
                forwardTo: { type: string, format: email }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  forwards:
                    type: array
                    items: { $ref: "#/components/schemas/Forward" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/mailboxes/{localPart}/forwards/{id}:
    delete:
      summary: Remove a forwarding address
      operationId: removeForward
      tags: [Forwarding]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
        - $ref: "#/components/parameters/IdParam"
      responses:
        "204": { description: Deleted }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/mailboxes/{localPart}/identity:
    get:
      summary: Get display name and signature
      operationId: getIdentity
      tags: [Identity]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Identity" }
    put:
      summary: Set display name and signature, applied to every outgoing message
      operationId: setIdentity
      tags: [Identity]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                displayName: { type: [string, "null"] }
                signatureText: { type: [string, "null"] }
                signatureHtml: { type: [string, "null"] }
      responses:
        "204": { description: Updated }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/mailboxes/{localPart}/vacation:
    get:
      summary: Get vacation / auto-reply settings
      operationId: getVacation
      tags: [Vacation]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Vacation" }
    put:
      summary: Set vacation / auto-reply settings
      operationId: setVacation
      tags: [Vacation]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [enabled]
              properties:
                enabled: { type: boolean }
                subject: { type: [string, "null"] }
                message: { type: [string, "null"], description: "Required if enabled is true" }
      responses:
        "204": { description: Updated }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/mailboxes/{localPart}/folders:
    get:
      summary: List custom folders for a mailbox
      operationId: listFolders
      tags: [Folders]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  folders:
                    type: array
                    items: { $ref: "#/components/schemas/Folder" }
    post:
      summary: Create a custom folder
      operationId: createFolder
      tags: [Folders]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Folder" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: "A folder with that name already exists in this mailbox" }

  /v1/domains/{domain}/mailboxes/{localPart}/folders/{id}:
    delete:
      summary: Delete a custom folder
      description: Does not delete the mail in it - messages fall back to their normal Inbox/Sent view.
      operationId: deleteFolder
      tags: [Folders]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
        - $ref: "#/components/parameters/IdParam"
      responses:
        "204": { description: Deleted }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/mailboxes/{localPart}/drafts:
    get:
      summary: List drafts (metadata only, no body)
      operationId: listDrafts
      tags: [Drafts]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  drafts:
                    type: array
                    items: { $ref: "#/components/schemas/DraftSummary" }
    post:
      summary: Create a draft
      operationId: createDraft
      tags: [Drafts]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DraftInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DraftDetail" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/mailboxes/{localPart}/drafts/{id}:
    get:
      summary: Get a single draft including body
      operationId: getDraft
      tags: [Drafts]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
        - $ref: "#/components/parameters/IdParam"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DraftDetail" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      summary: Partially update a draft (autosave-friendly)
      description: Only fields present in the request body are updated.
      operationId: updateDraft
      tags: [Drafts]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
        - $ref: "#/components/parameters/IdParam"
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DraftInput" }
      responses:
        "204": { description: Updated }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      summary: Delete a draft
      operationId: deleteDraft
      tags: [Drafts]
      parameters:
        - $ref: "#/components/parameters/DomainParam"
        - $ref: "#/components/parameters/LocalPartParam"
        - $ref: "#/components/parameters/IdParam"
      responses:
        "204": { description: Deleted }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/contacts:
    get:
      summary: List contacts (account-level, not per-mailbox)
      operationId: listContacts
      tags: [Contacts]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  contacts:
                    type: array
                    items: { $ref: "#/components/schemas/Contact" }
    post:
      summary: Create a contact
      operationId: createContact
      tags: [Contacts]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                name: { type: [string, "null"] }
                email: { type: string, format: email }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "409": { description: Already in contacts }

  /v1/contacts/{id}:
    put:
      summary: Partially update a contact
      operationId: updateContact
      tags: [Contacts]
      parameters:
        - $ref: "#/components/parameters/IdParam"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: [string, "null"] }
                email: { type: string, format: email }
      responses:
        "204": { description: Updated }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      summary: Delete a contact
      operationId: deleteContact
      tags: [Contacts]
      parameters:
        - $ref: "#/components/parameters/IdParam"
      responses:
        "204": { description: Deleted }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/emails:
    post:
      summary: Send a real email
      description: The from address's domain must already be registered to this account. Identity signature is applied automatically.
      operationId: sendEmail
      tags: [Sending]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [from, to, subject]
              properties:
                from: { type: string, format: email }
                to: { type: string, format: email }
                subject: { type: string }
                text: { type: string, description: "Plain-text body - text or html required" }
                html: { type: string, description: "HTML body - text or html required" }
                attachments:
                  type: array
                  items:
                    type: object
                    required: [filename, content]
                    properties:
                      filename: { type: string }
                      contentType: { type: string }
                      content: { type: string, description: "Base64-encoded, max 10MB decoded per attachment" }
                draftId: { type: string, format: uuid, description: "Deletes this draft on successful send" }
      responses:
        "202":
          description: Accepted for delivery
          content:
            application/json:
              schema:
                type: object
                properties:
                  messageId: { type: string }
                  envelope:
                    type: object
                    properties:
                      from: { type: string }
                      to: { type: array, items: { type: string } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "402": { $ref: "#/components/responses/LimitReached" }

  /v1/unread-counts:
    get:
      summary: Unread Inbox count per domain/mailbox, in one grouped query
      operationId: getUnreadCounts
      tags: [Messages]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  counts:
                    type: array
                    items: { $ref: "#/components/schemas/UnreadCount" }

  /v1/messages:
    get:
      summary: List messages (one row per thread) for a mailbox
      operationId: listMessages
      tags: [Messages]
      parameters:
        - name: mailbox
          in: query
          required: true
          schema: { type: string }
          description: "local@domain, must be owned by this account"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - name: direction
          in: query
          schema: { type: string, enum: [inbound, outbound] }
        - name: trash
          in: query
          schema: { type: string, enum: ["1"] }
          description: "Pass 1 to view Trash instead of the active mailbox"
        - name: folder
          in: query
          schema: { type: string, format: uuid }
          description: "View a custom folder instead - ignores direction"
        - name: q
          in: query
          schema: { type: string }
          description: "Full-text search over subject, from/to, and body"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  messages:
                    type: array
                    items: { $ref: "#/components/schemas/MessageSummary" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /v1/threads/{threadKey}:
    get:
      summary: List every individual message in one thread, oldest first
      operationId: getThread
      tags: [Messages]
      parameters:
        - name: threadKey
          in: path
          required: true
          schema: { type: string }
        - name: mailbox
          in: query
          required: true
          schema: { type: string }
        - name: direction
          in: query
          schema: { type: string, enum: [inbound, outbound] }
        - name: trash
          in: query
          schema: { type: string, enum: ["1"] }
        - name: folder
          in: query
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  messages:
                    type: array
                    items: { $ref: "#/components/schemas/MessageBase" }
        "400": { $ref: "#/components/responses/BadRequest" }

  /v1/messages/{id}:
    get:
      summary: Get one message's full content
      description: Marks it read as a side effect if it wasn't already.
      operationId: getMessage
      tags: [Messages]
      parameters:
        - $ref: "#/components/parameters/IdParam"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessageDetail" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      summary: Update message state (read/unread, restore, flag, move to folder)
      description: Any combination in one call. At least one field required.
      operationId: updateMessage
      tags: [Messages]
      parameters:
        - $ref: "#/components/parameters/IdParam"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                read: { type: boolean }
                deleted:
                  type: boolean
                  enum: [false]
                  description: "Only false is accepted here (restore from trash) - use DELETE to delete"
                flagged: { type: boolean }
                folderId:
                  type: [string, "null"]
                  format: uuid
                  description: "Must belong to the same mailbox as the message, or null"
      responses:
        "204": { description: Updated }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      summary: Delete a message (two-stage - trash, then permanent)
      description: First call soft-deletes (moves to Trash). A second call on an already-trashed message permanently deletes it.
      operationId: deleteMessage
      tags: [Messages]
      parameters:
        - $ref: "#/components/parameters/IdParam"
      responses:
        "204": { description: Deleted }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/messages/{id}/attachments/{index}:
    get:
      summary: Download one attachment's raw bytes
      operationId: getAttachment
      tags: [Messages]
      parameters:
        - $ref: "#/components/parameters/IdParam"
        - name: index
          in: path
          required: true
          schema: { type: integer, minimum: 0 }
          description: "Position in the attachments array from GET /v1/messages/{id}"
      responses:
        "200":
          description: Binary attachment content
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: "mek_live_..."
      description: >
        Bearer token API key, minted per-account. Shown once at creation,
        only a SHA-256 hash is stored server-side. Dashboard requests may
        alternatively authenticate via a Clerk session, not relevant for
        programmatic/agent integration.

  parameters:
    DomainParam:
      name: domain
      in: path
      required: true
      schema: { type: string }
      example: example.com
    LocalPartParam:
      name: localPart
      in: path
      required: true
      schema: { type: string }
      example: hello
    IdParam:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Not found on this account (tenant-isolated - never reveals another account's data)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    LimitReached:
      description: Billing tier limit reached
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string }

    Domain:
      type: object
      properties:
        domain: { type: string }
        verified: { type: boolean }
        created_at: { type: string, format: date-time }

    DnsRecord:
      type: object
      properties:
        type: { type: string, enum: [MX, TXT] }
        host: { type: string }
        value: { type: string }
        priority: { type: integer }
        label: { type: string }

    Recommendation:
      type: object
      properties:
        nameservers: { type: array, items: { type: string } }
        detectedHost: { type: [string, "null"], enum: [cloudflare, "simply.com", godaddy, route53, vercel, null] }
        action: { type: string, enum: [delegate, push-to-known-host, manual] }
        note: { type: string, description: "Present only if NS lookup failed" }

    CreateDomainResult:
      type: object
      properties:
        domain: { $ref: "#/components/schemas/Domain" }
        dkimKeyPath: { type: string }
        records:
          type: array
          items: { $ref: "#/components/schemas/DnsRecord" }
        recommendation: { $ref: "#/components/schemas/Recommendation" }

    Mailbox:
      type: object
      properties:
        local_part: { type: string }
        created_at: { type: string, format: date-time }

    CreateMailboxResult:
      type: object
      properties:
        id: { type: string, format: uuid }
        local_part: { type: string }
        created_at: { type: string, format: date-time }
        domain: { type: string }
        address: { type: string, format: email }
        password: { type: string, description: "Shown once if auto-generated" }

    Forward:
      type: object
      properties:
        id: { type: string, format: uuid }
        forward_to: { type: string, format: email }

    Identity:
      type: object
      properties:
        display_name: { type: [string, "null"] }
        signature_text: { type: [string, "null"] }
        signature_html: { type: [string, "null"] }

    Vacation:
      type: object
      properties:
        vacation_enabled: { type: boolean }
        vacation_subject: { type: [string, "null"] }
        vacation_message: { type: [string, "null"] }

    Folder:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        created_at: { type: string, format: date-time }

    Contact:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: [string, "null"] }
        email: { type: string, format: email }
        created_at: { type: string, format: date-time }

    DraftInput:
      type: object
      properties:
        to: { type: string }
        subject: { type: string }
        text: { type: string }
        html: { type: string }

    DraftSummary:
      type: object
      properties:
        id: { type: string, format: uuid }
        to_address: { type: [string, "null"] }
        subject: { type: [string, "null"] }
        updated_at: { type: string, format: date-time }

    DraftDetail:
      allOf:
        - $ref: "#/components/schemas/DraftSummary"
        - type: object
          properties:
            body_text: { type: [string, "null"] }
            body_html: { type: [string, "null"] }

    MessageBase:
      type: object
      properties:
        id: { type: string, format: uuid }
        direction: { type: string, enum: [inbound, outbound] }
        from_address: { type: [string, "null"] }
        to_address: { type: [string, "null"] }
        subject: { type: [string, "null"] }
        size_bytes: { type: integer }
        received_at: { type: string, format: date-time }
        read_at: { type: [string, "null"], format: date-time }
        deleted_at: { type: [string, "null"], format: date-time }
        flagged_at: { type: [string, "null"], format: date-time }
        folder_id: { type: [string, "null"], format: uuid }

    MessageSummary:
      allOf:
        - $ref: "#/components/schemas/MessageBase"
        - type: object
          properties:
            thread_key: { type: string }
            thread_count: { type: integer }
            thread_has_unread: { type: boolean }

    MessageDetail:
      allOf:
        - $ref: "#/components/schemas/MessageBase"
        - type: object
          properties:
            thread_key: { type: string }
            text: { type: [string, "null"] }
            html:
              type: [string, "null"]
              description: "Attacker-controlled content - sanitize and isolate before rendering, never render directly"
            attachments:
              type: array
              items:
                type: object
                properties:
                  filename: { type: [string, "null"] }
                  contentType: { type: string }
                  size: { type: integer }

    UnreadCount:
      type: object
      properties:
        domain: { type: string }
        local_part: { type: string }
        unread: { type: integer }

    Usage:
      type: object
      properties:
        tier: { type: string, enum: [free, paid, enterprise] }
        limits:
          type: object
          properties:
            label: { type: string }
            domains: { type: [integer, "null"], description: "null represents unlimited (enterprise)" }
            emailsPerMonth: { type: [integer, "null"] }
            storageBytesPerDomain: { type: [integer, "null"] }
        domainCount: { type: integer }
        emailsThisMonth: { type: integer }
        storageByDomain:
          type: array
          items:
            type: object
            properties:
              domain_id: { type: string, format: uuid }
              domain: { type: string }
              bytes: { type: string, description: "Numeric string (bigint)" }

    ApiKeySummary:
      type: object
      properties:
        id: { type: string, format: uuid }
        key_prefix: { type: string, example: mek_live_ab12 }
        created_at: { type: string, format: date-time }
        revoked_at: { type: [string, "null"], format: date-time }

    CreateApiKeyResult:
      allOf:
        - $ref: "#/components/schemas/ApiKeySummary"
        - type: object
          properties:
            key: { type: string, description: "The full plaintext key - shown once, not recoverable later", example: mek_live_ab12cd34ef56... }

tags:
  - name: Account
  - name: ApiKeys
  - name: Billing
  - name: Domains
  - name: Mailboxes
  - name: Forwarding
  - name: Identity
  - name: Vacation
  - name: Folders
  - name: Contacts
  - name: Drafts
  - name: Sending
  - name: Messages
