{
  "openapi": "3.1.0",
  "info": {
    "title": "TempMaily API",
    "version": "1.0.0",
    "description": "Public REST API for TempMaily. Create disposable inboxes, read and delete their messages, and register outbound webhooks. All endpoints require a premium `xtm_` Bearer API key (create one in the dashboard). Paths are relative to the API base `{origin}/api/v1`, where `{origin}` is your deployment URL (e.g. https://tempmaily.co)."
  },
  "servers": [
    {
      "url": "/api/v1",
      "description": "API base. Prefix with your deployment origin (NEXT_PUBLIC_APP_URL), e.g. https://tempmaily.co/api/v1."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    { "name": "Inboxes", "description": "Create and list disposable inboxes." },
    { "name": "Messages", "description": "Read and delete messages in owned inboxes." },
    { "name": "Webhooks", "description": "Register outbound delivery webhooks." }
  ],
  "paths": {
    "/inboxes": {
      "post": {
        "tags": ["Inboxes"],
        "summary": "Create an inbox",
        "description": "Creates an inbox on the default/service pool or a caller-owned custom domain. The raw `token` is returned exactly once so API clients can also drive the realtime widget; only its hash is stored server-side.",
        "operationId": "createInbox",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateInboxRequest" },
              "examples": {
                "default": { "summary": "Default service domain, 1-day expiry", "value": {} },
                "custom": {
                  "summary": "Caller-owned custom domain, no expiry",
                  "value": { "customDomainId": "b1f2c3d4-...", "expiry": "never" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Inbox created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreatedInbox" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PremiumRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "get": {
        "tags": ["Inboxes"],
        "summary": "List inboxes",
        "description": "Returns the caller's inboxes, newest first.",
        "operationId": "listInboxes",
        "responses": {
          "200": {
            "description": "The caller's inboxes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["inboxes"],
                  "properties": {
                    "inboxes": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/InboxSummary" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PremiumRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/inboxes/{id}/messages": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Inbox id (must be owned by the key's account).",
          "schema": { "type": "string", "format": "uuid" }
        }
      ],
      "get": {
        "tags": ["Messages"],
        "summary": "List messages in an inbox",
        "description": "Returns message summaries (no body) for a caller-owned inbox, newest first. An inbox the caller does not own returns 404.",
        "operationId": "listMessages",
        "responses": {
          "200": {
            "description": "Messages in the inbox.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["messages"],
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/MessageSummary" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PremiumRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/messages/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Message id (its inbox must be owned by the key's account).",
          "schema": { "type": "string", "format": "uuid" }
        }
      ],
      "get": {
        "tags": ["Messages"],
        "summary": "Get a message",
        "description": "Returns a full message including sanitized `bodyHtml`, and marks it read. Remote images are stripped from `bodyHtml` unless `images=1`. Render `bodyHtml` inside a sandboxed iframe.",
        "operationId": "getMessage",
        "parameters": [
          {
            "name": "images",
            "in": "query",
            "required": false,
            "description": "Set to `1` to keep remote images in the sanitized HTML.",
            "schema": { "type": "string", "enum": ["1"] }
          }
        ],
        "responses": {
          "200": {
            "description": "The full message.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Message" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PremiumRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Messages"],
        "summary": "Delete a message",
        "description": "Permanently removes the message (stored attachments, the row, and the owner's freed quota). Ownership is enforced as in GET.",
        "operationId": "deleteMessage",
        "responses": {
          "204": { "description": "Message deleted." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PremiumRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks": {
      "post": {
        "tags": ["Webhooks"],
        "summary": "Register a webhook",
        "description": "Registers an outbound webhook. `url` must be a public https URL. The signing `secret` is returned exactly once and never echoed by GET. Up to 5 webhooks per account.",
        "operationId": "createWebhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateWebhookRequest" },
              "examples": {
                "default": {
                  "summary": "Register a delivery endpoint",
                  "value": { "url": "https://hooks.example.com/xtm" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook registered.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreatedWebhook" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PremiumRequired" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        },
        "callbacks": {
          "messageReceived": {
            "{$request.body#/url}": {
              "post": {
                "summary": "message.received delivery",
                "description": "Sent to the registered `url` for every message received on an owned inbox. Authenticate it by recomputing `X-Xtm-Signature` = hex(HMAC-SHA256(secret, rawBody)) over the exact raw request body and comparing in constant time. Deliveries are best-effort with a 5s timeout and no retries.",
                "parameters": [
                  {
                    "name": "X-Xtm-Signature",
                    "in": "header",
                    "required": true,
                    "description": "hex(HMAC-SHA256(webhook secret, raw request body)).",
                    "schema": { "type": "string" }
                  }
                ],
                "requestBody": {
                  "required": true,
                  "content": {
                    "application/json": {
                      "schema": { "$ref": "#/components/schemas/WebhookDelivery" }
                    }
                  }
                },
                "responses": {
                  "200": { "description": "Acknowledged. Any non-2xx is logged and not retried." }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": ["Webhooks"],
        "summary": "List webhooks",
        "description": "Returns the caller's webhooks, newest first. `secret` is never included.",
        "operationId": "listWebhooks",
        "responses": {
          "200": {
            "description": "The caller's webhooks.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["webhooks"],
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Webhook" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PremiumRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Webhooks"],
        "summary": "Delete a webhook",
        "description": "Removes a webhook by id. A webhook the caller does not own returns 404.",
        "operationId": "deleteWebhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["webhookId"],
                "properties": {
                  "webhookId": { "type": "string", "format": "uuid" }
                }
              },
              "examples": {
                "default": { "value": { "webhookId": "b1f2c3d4-..." } }
              }
            }
          }
        },
        "responses": {
          "204": { "description": "Webhook deleted." },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PremiumRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A premium API key. Send it as `Authorization: Bearer xtm_...`. Create and revoke keys in the dashboard."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed request (e.g. `invalid_expiry`, `invalid_domain`, `invalid_url`, `invalid_request`).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "invalid_url" }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, malformed, unknown, or revoked API key.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "invalid_key" }
          }
        }
      },
      "PremiumRequired": {
        "description": "The key's account has no active premium subscription.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "premium_required" }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist or is not owned by the caller.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "not_found" }
          }
        }
      },
      "Conflict": {
        "description": "The account already holds the maximum of 5 webhooks.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "webhook_limit" }
          }
        }
      },
      "RateLimited": {
        "description": "The key exceeded its budget of 300 requests per 60 seconds.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "rate_limited" }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "A stable machine-readable error code.",
            "examples": ["invalid_key", "premium_required", "rate_limited", "not_found"]
          }
        }
      },
      "CreateInboxRequest": {
        "type": "object",
        "description": "All fields optional. `customDomainId` (a caller-owned verified domain) takes precedence over `domainId` (a service-pool domain).",
        "properties": {
          "domainId": {
            "type": "string",
            "format": "uuid",
            "description": "A service-pool domain id. Defaults to the free service domain."
          },
          "customDomainId": {
            "type": "string",
            "format": "uuid",
            "description": "A caller-owned, verified custom domain id."
          },
          "expiry": {
            "type": "string",
            "enum": ["1d", "7d", "30d", "never"],
            "default": "1d",
            "description": "Inbox lifetime. `never` creates a permanent inbox."
          }
        }
      },
      "CreatedInbox": {
        "type": "object",
        "required": ["id", "address", "token", "expiresAt", "isPermanent"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "address": { "type": "string", "format": "email" },
          "token": {
            "type": "string",
            "description": "The raw inbox token, shown exactly once. Only its hash is stored."
          },
          "expiresAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When the inbox expires, or null if permanent."
          },
          "isPermanent": { "type": "boolean" }
        }
      },
      "InboxSummary": {
        "type": "object",
        "required": ["id", "address", "status", "createdAt", "expiresAt", "isPermanent"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "address": { "type": "string", "format": "email" },
          "status": {
            "type": "string",
            "enum": ["active", "reserved"],
            "description": "`reserved` = an expired premium inbox inside its restore grace window."
          },
          "createdAt": { "type": "string", "format": "date-time" },
          "expiresAt": { "type": ["string", "null"], "format": "date-time" },
          "isPermanent": { "type": "boolean" }
        }
      },
      "MessageSummary": {
        "type": "object",
        "required": [
          "id",
          "fromAddress",
          "fromName",
          "subject",
          "snippet",
          "isRead",
          "hasAttachments",
          "receivedAt"
        ],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "fromAddress": { "type": "string", "format": "email" },
          "fromName": { "type": ["string", "null"] },
          "subject": { "type": ["string", "null"] },
          "snippet": { "type": "string" },
          "isRead": { "type": "boolean" },
          "hasAttachments": { "type": "boolean" },
          "receivedAt": { "type": "string", "format": "date-time" }
        }
      },
      "Message": {
        "type": "object",
        "required": [
          "id",
          "fromAddress",
          "fromName",
          "subject",
          "bodyText",
          "bodyHtml",
          "snippet",
          "isRead",
          "hasAttachments",
          "sizeBytes",
          "receivedAt"
        ],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "fromAddress": { "type": "string", "format": "email" },
          "fromName": { "type": ["string", "null"] },
          "subject": { "type": ["string", "null"] },
          "bodyText": { "type": ["string", "null"] },
          "bodyHtml": {
            "type": ["string", "null"],
            "description": "Sanitized HTML (remote images stripped unless requested with images=1). Render inside a sandboxed iframe."
          },
          "snippet": { "type": "string" },
          "isRead": { "type": "boolean", "description": "Always true in this response — fetching marks read." },
          "hasAttachments": { "type": "boolean" },
          "sizeBytes": { "type": "integer" },
          "receivedAt": { "type": "string", "format": "date-time" }
        }
      },
      "CreateWebhookRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "A public https URL. Local/internal/private hosts are rejected (SSRF defense)."
          }
        }
      },
      "CreatedWebhook": {
        "type": "object",
        "required": ["id", "url", "secret", "enabled"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "secret": {
            "type": "string",
            "description": "The HMAC signing secret, shown exactly once. Store it to verify deliveries."
          },
          "enabled": { "type": "boolean" }
        }
      },
      "Webhook": {
        "type": "object",
        "required": ["id", "url", "enabled", "createdAt"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "enabled": { "type": "boolean" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "description": "The JSON body POSTed to a registered webhook url.",
        "required": ["event", "inboxId", "message"],
        "properties": {
          "event": { "type": "string", "const": "message.received" },
          "inboxId": { "type": "string", "format": "uuid" },
          "message": { "$ref": "#/components/schemas/WebhookMessage" }
        }
      },
      "WebhookMessage": {
        "type": "object",
        "required": [
          "id",
          "fromAddress",
          "fromName",
          "subject",
          "snippet",
          "receivedAt",
          "hasAttachments"
        ],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "fromAddress": { "type": "string", "format": "email" },
          "fromName": { "type": ["string", "null"] },
          "subject": { "type": ["string", "null"] },
          "snippet": { "type": "string" },
          "receivedAt": { "type": "string", "format": "date-time" },
          "hasAttachments": { "type": "boolean" }
        }
      }
    }
  }
}
