{
  "openapi": "3.1.0",
  "info": {
    "title": "SendBunny Transactional Email API",
    "version": "1.0.0",
    "summary": "Send single transactional emails from your self-hosted SendBunny install.",
    "description": "SendBunny is self-hosted: every install exposes its own unique endpoint (an AWS Lambda function URL) instead of a shared API host. Find yours in the SendBunny dashboard on the API Keys page, labeled 'Transactional API URL'. The API has one operation: POST a JSON body to that URL to send one email. Human-readable docs: https://sendbunny.co/docs/send-email-api",
    "contact": {
      "url": "https://sendbunny.co/docs"
    }
  },
  "servers": [
    {
      "url": "{transactionalApiUrl}",
      "description": "Your install's Transactional API URL (dashboard → API Keys), e.g. https://abc123xyz.lambda-url.us-east-1.on.aws/",
      "variables": {
        "transactionalApiUrl": {
          "default": "https://YOUR-INSTALL.lambda-url.us-east-1.on.aws/",
          "description": "Unique per install — SendBunny runs in your own AWS account."
        }
      }
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/": {
      "post": {
        "operationId": "sendEmail",
        "summary": "Send a transactional email",
        "description": "Sends exactly one email. Two modes: raw (inline subject + html/text) or template (templateId or templateAlias, with optional data merge variables). Modes cannot be mixed. The from address must belong to a verified sender identity on a root domain the API key is scoped to. Recipients on the account suppression list are rejected with 422. There is no idempotency support — only retry requests that certainly did not return 200 (429 is always retry-safe; it is emitted before any send). Any HTTP method other than POST returns 405. Unrecognized body fields (cc, bcc, replyTo, attachments, headers, ...) are silently ignored — none of those features exist on this endpoint today.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendEmailRequest"
              },
              "examples": {
                "raw": {
                  "summary": "Raw send",
                  "value": {
                    "from": "receipts@yourdomain.com",
                    "to": "customer@example.com",
                    "subject": "Your order shipped",
                    "html": "<h1>On its way</h1><p>Order #1042 shipped today.</p>",
                    "text": "On its way — order #1042 shipped today."
                  }
                },
                "template": {
                  "summary": "Template send with merge variables",
                  "value": {
                    "from": "hello@yourdomain.com",
                    "to": "priya@example.com",
                    "templateAlias": "password-reset",
                    "data": {
                      "name": "Priya",
                      "reset_url": "https://app.example.com/reset/abc123"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Email accepted by Amazon SES in your AWS account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "messageId"
                  ],
                  "properties": {
                    "messageId": {
                      "type": "string",
                      "description": "SES message id; correlates with delivery/bounce events in the dashboard."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed. The error message names the field. Extra fields when relevant: missing (template variables absent from data), invalid (data paths with unsupported values).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing Bearer header ('Unauthorized') or unknown/revoked key ('Invalid API key').",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Domain or identity not allowed: from-domain or template-domain outside the key's scope (response includes allowedRootDomains), or the from address is not a verified sender identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Template not found (bad id, or alias not present on the from address's root domain).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Recipient is on the suppression list. The reason field is BOUNCE, COMPLAINT, or MANUAL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Throttled by AWS Lambda when the install caps endpoint concurrency (an install-time option, cap 10). Emitted by AWS before any send happens, so a 429 is always safe to retry with exponential backoff. Body shape is AWS-defined — do not rely on it."
          },
          "502": {
            "description": "Amazon SES rejected the send (message passed through) — commonly SES sandbox restrictions or sending-quota limits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sb_ API key",
        "description": "API key created by an ADMIN in dashboard → API Keys. Starts with 'sb_', shown once at creation, scoped to specific sending root domains. Server-side secret — never call this API from browser JavaScript."
      }
    },
    "schemas": {
      "SendEmailRequest": {
        "description": "Exactly one of the two send modes. Raw: subject + at least one of html/text, no template fields. Template: exactly one of templateId/templateAlias, no html/text, optional data.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/RawSendRequest"
          },
          {
            "$ref": "#/components/schemas/TemplateSendRequest"
          }
        ]
      },
      "RawSendRequest": {
        "type": "object",
        "required": [
          "from",
          "to",
          "subject"
        ],
        "anyOf": [
          {
            "required": [
              "html"
            ]
          },
          {
            "required": [
              "text"
            ]
          }
        ],
        "not": {
          "anyOf": [
            {
              "required": [
                "templateId"
              ]
            },
            {
              "required": [
                "templateAlias"
              ]
            },
            {
              "required": [
                "data"
              ]
            }
          ]
        },
        "description": "Inline send: subject plus html and/or text. No plain-text part is auto-derived from html.",
        "properties": {
          "from": {
            "$ref": "#/components/schemas/FromAddress"
          },
          "to": {
            "$ref": "#/components/schemas/ToAddress"
          },
          "subject": {
            "type": "string",
            "minLength": 1,
            "description": "Email subject (non-empty)."
          },
          "html": {
            "type": "string",
            "description": "HTML body. At least one of html/text is required."
          },
          "text": {
            "type": "string",
            "description": "Plain-text body. Providing both parts is recommended."
          }
        }
      },
      "TemplateSendRequest": {
        "type": "object",
        "required": [
          "from",
          "to"
        ],
        "oneOf": [
          {
            "required": [
              "templateId"
            ]
          },
          {
            "required": [
              "templateAlias"
            ]
          }
        ],
        "not": {
          "anyOf": [
            {
              "required": [
                "html"
              ]
            },
            {
              "required": [
                "text"
              ]
            }
          ]
        },
        "description": "Send a stored template (created in dashboard → Templates) with optional merge variables.",
        "properties": {
          "from": {
            "$ref": "#/components/schemas/FromAddress"
          },
          "to": {
            "$ref": "#/components/schemas/ToAddress"
          },
          "subject": {
            "type": "string",
            "description": "Optional — a non-empty value overrides the template's stored subject."
          },
          "templateId": {
            "type": "string",
            "description": "Stored template id. Mutually exclusive with templateAlias."
          },
          "templateAlias": {
            "type": "string",
            "description": "Stored template alias, lowercased and resolved on the from address's root domain. Mutually exclusive with templateId."
          },
          "data": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/TemplateDataValue"
            },
            "description": "Merge variables. Nested objects flatten to dot paths ({user:{name}} fills {{user.name}}); numbers/booleans are stringified. {{email}} defaults to the to address. Every template variable without an inline fallback must be present, else 400 with missing[]."
          }
        }
      },
      "TemplateDataValue": {
        "description": "Strings, numbers, booleans, or nested objects of those. Arrays and null are rejected (400 with invalid[]).",
        "anyOf": [
          {
            "type": "string"
          },
          {
            "type": "number"
          },
          {
            "type": "boolean"
          },
          {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/TemplateDataValue"
            }
          }
        ]
      },
      "FromAddress": {
        "type": "string",
        "format": "email",
        "description": "Sender as a bare address (display-name forms like 'Ann <a@b.com>' are rejected). Must be on a verified sender identity, on a root domain the API key allows."
      },
      "ToAddress": {
        "type": "string",
        "format": "email",
        "description": "Recipient as a bare address — exactly one per request. Lowercased before processing and checked against the account suppression list."
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "allowedRootDomains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "On 403 domain-scope errors: root domains this key may use."
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "On 400 template errors: required variables absent from data."
          },
          "invalid": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "On 400 data errors: dot paths with unsupported value types."
          },
          "reason": {
            "type": "string",
            "enum": [
              "BOUNCE",
              "COMPLAINT",
              "MANUAL"
            ],
            "description": "On 422: why the recipient is suppressed."
          }
        }
      }
    }
  }
}