{
  "openapi": "3.1.0",
  "info": {
    "title": "Sendlet send API",
    "version": "1.0.0",
    "description": "Each endpoint URL sends email to exactly one pre-approved recipient. The URL is the permission: keep it secret. GET describes the endpoint; POST sends."
  },
  "servers": [{ "url": "https://sendlet.email" }],
  "paths": {
    "/to/{address}": {
      "parameters": [
        {
          "name": "address",
          "in": "path",
          "required": true,
          "description": "The slug and secret token, e.g. sam-k7F2x9Qa4mB8nR1vT6yZ3c. The token is the 22 base62 characters after the last hyphen.",
          "schema": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*-[0-9A-Za-z]{22}$" }
        }
      ],
      "get": {
        "operationId": "describeEndpoint",
        "summary": "Describe the endpoint (never sends unless GET-send mode is on and send=yes is given).",
        "parameters": [
          { "name": "send", "in": "query", "required": false, "description": "GET-send mode only: must be \"yes\".", "schema": { "type": "string", "enum": ["yes"] } },
          { "name": "subject", "in": "query", "required": false, "schema": { "type": "string", "maxLength": 200 } },
          { "name": "body", "in": "query", "required": false, "schema": { "type": "string", "maxLength": 50000 } }
        ],
        "responses": {
          "200": {
            "description": "Instructions for this endpoint.",
            "content": {
              "text/markdown": { "schema": { "type": "string" } },
              "application/json": { "schema": { "$ref": "#/components/schemas/EndpointDescription" } }
            }
          },
          "202": { "$ref": "#/components/responses/Accepted" },
          "404": { "$ref": "#/components/responses/Problem" },
          "410": { "$ref": "#/components/responses/Problem" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "operationId": "sendEmail",
        "summary": "Send an email to this endpoint's recipient.",
        "parameters": [
          { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,100}$" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["subject", "body"],
                "properties": {
                  "subject": { "type": "string", "maxLength": 200 },
                  "body": { "type": "string", "maxLength": 50000 },
                  "idempotency_key": { "type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,100}$" },
                  "file": { "type": "array", "maxItems": 10, "items": { "type": "string", "contentMediaType": "application/octet-stream" }, "description": "Any part with a file name is an attachment, whatever the part is called." }
                }
              }
            },
            "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/SendRequest" } },
            "text/plain": { "schema": { "type": "string", "description": "First line is the subject; the rest is the body." } },
            "text/markdown": { "schema": { "type": "string", "description": "First line is the subject; the rest is the body." } }
          }
        },
        "responses": {
          "202": { "$ref": "#/components/responses/Accepted" },
          "200": {
            "description": "Duplicate of a send in the last 10 minutes; nothing new was sent.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendAccepted" } } }
          },
          "400": { "$ref": "#/components/responses/Problem" },
          "404": { "$ref": "#/components/responses/Problem" },
          "409": { "$ref": "#/components/responses/Problem" },
          "410": { "$ref": "#/components/responses/Problem" },
          "413": { "$ref": "#/components/responses/Problem" },
          "415": { "$ref": "#/components/responses/Problem" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/to/{address}/sends/{id}": {
      "parameters": [
        {
          "name": "address",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*-[0-9A-Za-z]{22}$" }
        },
        { "name": "id", "in": "path", "required": true, "description": "The id from the send's 202 response.", "schema": { "type": "string", "pattern": "^snd_[0-9a-f]{32}$" } }
      ],
      "get": {
        "operationId": "getSendStatus",
        "summary": "What happened to a send made through this endpoint, per recipient (labels only, never addresses).",
        "responses": {
          "200": {
            "description": "The send's status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string" },
                    "status": { "type": "string", "enum": ["awaiting_approval", "rejected", "queued", "sent", "partly_sent", "failed"] },
                    "to": { "type": "string" },
                    "created_at": { "type": "string", "format": "date-time" },
                    "recipients": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "to": { "type": "string" },
                          "status": { "type": "string", "enum": ["awaiting_approval", "rejected", "queued", "sent", "bounced", "complained", "failed", "skipped"] },
                          "sent_at": { "type": "string", "format": "date-time" },
                          "detail": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/mcp/{address}": {
      "parameters": [
        {
          "name": "address",
          "in": "path",
          "required": true,
          "description": "The same slug and secret token as the send URL.",
          "schema": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*-[0-9A-Za-z]{22}$" }
        }
      ],
      "post": {
        "operationId": "mcp",
        "summary": "Model Context Protocol server for this endpoint (Streamable HTTP, stateless).",
        "description": "One JSON-RPC 2.0 message per request: initialize, ping, tools/list, tools/call. Tools: send_email and endpoint_info. Notifications return 202. Use an MCP client rather than calling this directly.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "description": "A JSON-RPC 2.0 request or notification." } } }
        },
        "responses": {
          "200": { "description": "JSON-RPC response.", "content": { "application/json": { "schema": { "type": "object" } } } },
          "202": { "description": "Notification accepted." },
          "400": { "description": "Unsupported MCP-Protocol-Version." },
          "404": { "$ref": "#/components/responses/Problem" },
          "410": { "$ref": "#/components/responses/Problem" },
          "413": { "$ref": "#/components/responses/Problem" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SendRequest": {
        "type": "object",
        "required": ["subject", "body"],
        "properties": {
          "subject": { "type": "string", "minLength": 1, "maxLength": 200, "description": "Alias: title." },
          "body": { "type": "string", "minLength": 1, "maxLength": 50000, "description": "Plain text or Markdown. Aliases: message, text, content. Show an attached image with ![Description](filename.png)." },
          "idempotency_key": { "type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,100}$" },
          "attachments": {
            "type": "array",
            "maxItems": 10,
            "description": "JSON only. Alias: files. At most 5 MB in total. Allowed extensions: png, jpg, jpeg, gif, webp, pdf, txt, csv, md, json, ics, docx, xlsx, pptx, odt, ods.",
            "items": {
              "type": "object",
              "required": ["filename", "content"],
              "properties": {
                "filename": { "type": "string", "maxLength": 100, "description": "Aliases: name, file_name." },
                "content": { "type": "string", "contentEncoding": "base64", "description": "Base64, or a data: URL. Aliases: data, base64." }
              }
            }
          }
        }
      },
      "SendAccepted": {
        "type": "object",
        "required": ["sent", "id", "to", "remaining_today"],
        "properties": {
          "sent": { "type": "boolean" },
          "id": { "type": "string", "example": "snd_0192a4c5d1e87b3f9c2d4e6f8a0b1c2d" },
          "to": { "type": "string", "example": "Sam (personal)" },
          "remaining_today": { "type": "integer" },
          "duplicate": { "type": "boolean" },
          "recipients": { "type": "integer", "description": "Group endpoints only: how many members were emailed. Each member gets a separate copy, and each counts towards the daily limits." },
          "awaiting_approval": { "type": "boolean", "description": "True when the owner must approve the email before it is sent. Don't resend it." }
        }
      },
      "EndpointDescription": {
        "type": "object",
        "properties": {
          "to": { "type": "string" },
          "url": { "type": "string" },
          "method": { "type": "string" },
          "content_types": { "type": "array", "items": { "type": "string" } },
          "subject_max_length": { "type": "integer" },
          "body_max_length": { "type": "integer" },
          "daily_limit": { "type": "integer" },
          "used_today": { "type": "integer" },
          "markdown": { "type": "boolean" },
          "get_send_enabled": { "type": "boolean" },
          "group_members": { "type": "integer", "description": "Group endpoints only: members who will each get their own copy." },
          "mcp_url": { "type": "string", "description": "The same endpoint as an MCP server." },
          "status_url": { "type": "string", "description": "Where to check a send: replace {id} with its snd_ id." },
          "requires_approval": { "type": "boolean" },
          "attachments": {
            "type": "object",
            "properties": {
              "field": { "type": "array", "items": { "type": "string" } },
              "max_count": { "type": "integer" },
              "max_total_bytes": { "type": "integer" },
              "types": { "type": "array", "items": { "type": "string" } },
              "inline": { "type": "string" },
              "example": { "type": "string" }
            }
          },
          "example": { "type": "string" }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details with a hint showing a corrected request.",
        "properties": {
          "type": { "type": "string" },
          "title": { "type": "string" },
          "status": { "type": "integer" },
          "hint": { "type": "string" },
          "code": { "type": "string" }
        }
      }
    },
    "responses": {
      "Accepted": {
        "description": "Queued for delivery.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendAccepted" } } }
      },
      "Problem": {
        "description": "Problem details.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "RateLimited": {
        "description": "Limit reached. Wait for Retry-After seconds.",
        "headers": { "Retry-After": { "schema": { "type": "integer" } } },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      }
    }
  }
}
