openapi: 3.1.0
info:
  title: SnappyMails Public API
  version: 2.0.0
  summary: Brevo-compatible transactional email, contacts, lists, behavioral events, templates and WhatsApp messaging.
  description: |
    The SnappyMails public API is authenticated with a per-brand API key sent in the `api-key` header.

    * Errors are returned as an HTTP status plus `{"code", "message"}`.
    * Every brand has a token-bucket rate limit (Starter 15/s, Business 50/s, Enterprise 150/s).
      Exceeding it returns `429 too_many_requests` with a `Retry-After` header (seconds).
    * API keys may be restricted to the permissions `smtp:send`, `templates:read`, `contacts:write`,
      `events:write` and `whatsapp:send`. A key without any permission has full access.

    Human-readable documentation: /docs
servers:
  - url: https://api.snappymails.com
    description: Production
  - url: http://localhost:8080
    description: Local development
security:
  - apiKey: []
tags:
  - name: Transactional email
  - name: Templates
  - name: Contacts & lists
  - name: Behavioral events
  - name: WhatsApp
  - name: Webhooks
    description: Signed event notifications POSTed to your HTTPS endpoints (see the top-level `webhooks` section).

paths:
  /v3/smtp/email:
    post:
      tags: [Transactional email]
      operationId: sendTransacEmail
      summary: Send a transactional email
      description: |
        Brevo v3 compatible. Requires the `smtp:send` permission. The sender email must belong to a
        verified sender domain. Each recipient (to, cc and bcc) counts as one email towards the monthly quota.

        Optional `attachment` (base64 `content` or `url`, at most 10 files, 5 MB each, 10 MB total) and
        `scheduledAt` (up to 72 hours ahead; the response is then `202`). Quota is reserved at request time.
      requestBody:
        $ref: "#/components/requestBodies/SendEmail"
      responses:
        "201":
          $ref: "#/components/responses/EmailCreated"
        "202":
          $ref: "#/components/responses/EmailScheduled"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/QuotaExceeded"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/tx/send:
    post:
      tags: [Transactional email]
      operationId: sendTransacEmailAlias
      summary: Send a transactional email (alias)
      description: Alias of `POST /v3/smtp/email` with the same body, responses and errors.
      requestBody:
        $ref: "#/components/requestBodies/SendEmail"
      responses:
        "201":
          $ref: "#/components/responses/EmailCreated"
        "202":
          $ref: "#/components/responses/EmailScheduled"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/QuotaExceeded"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v3/smtp/email/{identifier}:
    delete:
      tags: [Transactional email]
      operationId: deleteScheduledEmailById
      summary: Cancel a scheduled email or batch
      description: |
        Cancels scheduled sends by `messageId` (with or without `<...>` / `@snappymails`) or by `batchId`
        (every still-scheduled message of the batch). The reserved quota is released.
        Requires the `smtp:send` permission.
      parameters:
        - $ref: "#/components/parameters/EmailIdentifier"
      responses:
        "204":
          description: The scheduled send(s) were cancelled.
        "400":
          description: The message(s) are no longer scheduled (already sent or cancelled) (`invalid_parameter`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                code: invalid_parameter
                message: The message is no longer scheduled
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No scheduled message or batch matches the identifier (`document_not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                code: document_not_found
                message: Scheduled email not found
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v3/smtp/emailStatus/{identifier}:
    get:
      tags: [Transactional email]
      operationId: getScheduledEmailById
      summary: Get the status of a scheduled email or batch
      description: |
        Returns the scheduling status of a message (by `messageId`) or of every message of a batch
        (by `batchId`). Requires the `smtp:send` permission.
      parameters:
        - $ref: "#/components/parameters/EmailIdentifier"
      responses:
        "200":
          description: The scheduling status.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ScheduledEmailStatus"
                  - $ref: "#/components/schemas/ScheduledBatchStatus"
              examples:
                message:
                  summary: By messageId
                  value:
                    scheduledAt: "2026-10-18T09:00:00+08:00"
                    createdAt: "2026-10-17T10:15:02Z"
                    status: queued
                batch:
                  summary: By batchId
                  value:
                    count: 2
                    batches:
                      - scheduledAt: "2026-10-18T09:00:00+08:00"
                        createdAt: "2026-10-17T10:15:02Z"
                        status: queued
                      - scheduledAt: "2026-10-18T09:00:00+08:00"
                        createdAt: "2026-10-17T10:15:02Z"
                        status: cancelled
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v3/smtp/templates:
    get:
      tags: [Templates]
      operationId: getSmtpTemplates
      summary: List templates
      description: Returns every template of the brand. Requires the `templates:read` permission.
      responses:
        "200":
          description: The brand's templates.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplateList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v3/senders/ips:
    get:
      tags: [Transactional email]
      operationId: getDedicatedIps
      summary: List dedicated IPs & AWS SES IP pools
      description: Returns the Dedicated IPs, AWS SES Pool names, warm-up progress, and bound domains for the brand. Requires `smtp:send`.
      responses:
        "200":
          description: Dedicated IPs assigned to the brand.
          content:
            application/json:
              schema:
                type: object
                required: [ips]
                properties:
                  ips:
                    type: array
                    items:
                      type: object
                      required: [id, ip, domain, weight, status, warmup]
                      properties:
                        id:
                          type: string
                          format: uuid
                        ip:
                          type: string
                        domain:
                          type: string
                        rdns:
                          type: string
                        weight:
                          type: integer
                        ses_pool_name:
                          type: string
                        status:
                          type: string
                          enum: [PENDING, WARMING, ACTIVE, PAUSED]
                        warmup:
                          type: object
                          properties:
                            status:
                              type: string
                              enum: [IN_PROGRESS, DONE]
                            percentage:
                              type: integer
                        associated_domains:
                          type: array
                          items:
                            type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/whatsapp/send:
    post:
      tags: [WhatsApp]
      operationId: sendWhatsApp
      summary: Send a WhatsApp message
      description: |
        Requires the `whatsapp:send` permission and a connected WhatsApp Business number.
        Free-form `text` messages are only allowed within 24 hours of the customer's last inbound
        message (the customer service window); otherwise use a `template`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WhatsAppSendRequest"
            examples:
              template:
                summary: Template message
                value:
                  to: "+60123456789"
                  type: template
                  template:
                    name: order_update
                    language: en_US
                    parameters:
                      body: ["A-1077", "RM 42.50"]
              text:
                summary: Text message (inside the 24h window)
                value:
                  to: "+60123456789"
                  type: text
                  text:
                    body: "Your parcel is out for delivery."
                    preview_url: false
      responses:
        "201":
          description: The message was queued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppSendResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: No WhatsApp number is connected for this brand (`whatsapp_not_connected`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                code: whatsapp_not_connected
                message: Connect a WhatsApp number first
        "422":
          description: Free-form text outside the 24-hour customer service window (`outside_service_window`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                code: outside_service_window
                message: The 24-hour customer service window is closed; send a template instead
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/whatsapp/messages/{messageId}:
    get:
      tags: [WhatsApp]
      operationId: getWhatsAppMessage
      summary: Get a WhatsApp message
      description: Returns the message and its current status. Requires the `whatsapp:send` permission.
      parameters:
        - name: messageId
          in: path
          required: true
          description: The `messageId` returned by `POST /v1/whatsapp/send`.
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The message.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WaMessage"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalError"

  /v3/contacts:
    get:
      tags: [Contacts & lists]
      operationId: getContacts
      summary: List contacts
      description: Returns a paginated list of contacts for the brand. Requires `contacts:write`.
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 1000
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
            minimum: 0
        - name: email
          in: query
          description: Filter contacts by email substring.
          schema:
            type: string
      responses:
        "200":
          description: Paginated contact list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BrevoContactList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
    post:
      tags: [Contacts & lists]
      operationId: createContact
      summary: Create or upsert a contact
      description: |
        Creates a contact or updates an existing contact when `updateEnabled: true` is passed.
        Adding a contact to `listIds` schedules active autoresponders and triggers active `list_joined` workflows.
        Requires `contacts:write`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateContactRequest"
      responses:
        "201":
          description: Contact created.
          content:
            application/json:
              schema:
                type: object
                required: [id]
                properties:
                  id:
                    type: string
                    format: uuid
        "200":
          description: Existing contact updated (`updateEnabled: true`).
          content:
            application/json:
              schema:
                type: object
                required: [id, updated]
                properties:
                  id:
                    type: string
                    format: uuid
                  updated:
                    type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /v3/contacts/{identifier}:
    get:
      tags: [Contacts & lists]
      operationId: getContactInfo
      summary: Get a contact by UUID or email
      description: Returns a contact by UUID or URL-encoded email address. Requires `contacts:write`.
      parameters:
        - $ref: "#/components/parameters/ContactIdentifier"
      responses:
        "200":
          description: Contact details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BrevoContact"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      tags: [Contacts & lists]
      operationId: updateContact
      summary: Update a contact
      description: Merges attributes, updates subscription status, or links/unlinks lists. Requires `contacts:write`.
      parameters:
        - $ref: "#/components/parameters/ContactIdentifier"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateContactRequest"
      responses:
        "204":
          description: Contact updated.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Contacts & lists]
      operationId: deleteContact
      summary: Delete a contact
      description: Deletes a contact by UUID or URL-encoded email address. Requires `contacts:write`.
      parameters:
        - $ref: "#/components/parameters/ContactIdentifier"
      responses:
        "204":
          description: Contact deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /v3/contacts/lists:
    get:
      tags: [Contacts & lists]
      operationId: getLists
      summary: List subscriber lists
      description: Returns all subscriber lists for the brand. Requires `contacts:write`.
      responses:
        "200":
          description: Subscriber lists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BrevoListCollection"
    post:
      tags: [Contacts & lists]
      operationId: createList
      summary: Create a subscriber list
      description: Creates a new subscriber list. Requires `contacts:write`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
      responses:
        "201":
          description: List created.
          content:
            application/json:
              schema:
                type: object
                required: [id, name]
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string

  /v3/contacts/lists/{listId}/contacts/add:
    post:
      tags: [Contacts & lists]
      operationId: addContactToList
      summary: Add existing contacts to a list
      description: Adds contacts to a list by email or UUID and triggers autoresponders and `list_joined` workflows. Requires `contacts:write`.
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListContactsMutation"
      responses:
        "201":
          description: Contacts processed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListContactsMutationResult"

  /v3/contacts/lists/{listId}/contacts/remove:
    post:
      tags: [Contacts & lists]
      operationId: removeContactFromList
      summary: Remove contacts from a list
      description: Removes contacts from a list by email or UUID. Requires `contacts:write`.
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListContactsMutation"
      responses:
        "201":
          description: Contacts processed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListContactsMutationResult"

  /v3/events:
    post:
      tags: [Behavioral events]
      operationId: trackEvent
      summary: Track a custom behavioral event
      description: |
        Records a custom event (`cart_abandoned`, `order_completed`, `trial_started`, etc.) for a contact.
        Upserts the contact and any `contact_properties` / `attributes`, and immediately triggers any active
        automation workflows matching `event_name`. Requires `events:write`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TrackEventRequest"
      responses:
        "201":
          description: Event recorded and matching workflows triggered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackEventResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /v1/events:
    post:
      tags: [Behavioral events]
      operationId: trackEventAlias
      summary: Track a custom behavioral event (alias)
      description: Alias of `POST /v3/events`. Requires `events:write`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TrackEventRequest"
      responses:
        "201":
          description: Event recorded and matching workflows triggered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrackEventResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: api-key
      description: A SnappyMails API key (`sm_live_...`), created in the portal under API Keys & SMTP.

  parameters:
    EmailIdentifier:
      name: identifier
      in: path
      required: true
      description: A `messageId` (with or without `<...>` / `@snappymails`) or a `batchId` (UUID).
      schema:
        type: string
      examples:
        messageId:
          value: "<3f1c2a9e-8b7d-4d61-9a63-0c2f5e7b1d44@snappymails>"
        batchId:
          value: 5c6a2f0e-7d1b-4e3a-9f2c-8b1d0e4a6c11
    ContactIdentifier:
      name: identifier
      in: path
      required: true
      description: Contact UUID or URL-encoded email address (e.g. `jane%40example.com`).
      schema:
        type: string

  headers:
    Retry-After:
      description: Number of seconds to wait before retrying.
      schema:
        type: integer
        minimum: 1

  requestBodies:
    SendEmail:
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/SendEmailRequest"
          examples:
            html:
              summary: HTML content with params
              value:
                sender:
                  name: Acme Shop
                  email: orders@acme.com
                to:
                  - email: jane@example.com
                    name: Jane Doe
                subject: "Order {{ params.ORDER_ID }} confirmed"
                htmlContent: "<p>Thanks, {{ params.NAME }}!</p>"
                params:
                  NAME: Jane
                  ORDER_ID: A-1077
                tags: [order-confirmation]
            template:
              summary: Saved template
              value:
                sender:
                  email: orders@acme.com
                to:
                  - email: jane@example.com
                templateId: 12
                params:
                  ORDER_ID: A-1077
            messageVersions:
              summary: Batch sending
              value:
                sender:
                  email: news@acme.com
                templateId: 7
                params:
                  PROMO: AUTUMN20
                messageVersions:
                  - to:
                      - email: jane@example.com
                    params:
                      NAME: Jane
                  - to:
                      - email: omar@example.com
                    params:
                      NAME: Omar
                      PROMO: VIP30
            attachment:
              summary: With attachments
              value:
                sender:
                  email: orders@acme.com
                to:
                  - email: jane@example.com
                subject: Your invoice A-77
                htmlContent: "<p>Your invoice is attached.</p>"
                attachment:
                  - content: JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFI+PgpzdHJlYW0K
                    name: invoice-A77.pdf
                  - url: https://cdn.acme.com/terms.pdf
                    name: terms.pdf
            scheduled:
              summary: Scheduled send
              value:
                sender:
                  email: orders@acme.com
                to:
                  - email: jane@example.com
                subject: Your appointment tomorrow
                htmlContent: "<p>See you tomorrow at 10:00.</p>"
                scheduledAt: "2026-10-18T09:00:00+08:00"

  responses:
    EmailCreated:
      description: |
        The message(s) were queued. `messageId` is returned for a single message,
        `messageIds` (one per version, in order) when `messageVersions` is used.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/SendEmailResponse"
          examples:
            single:
              value:
                messageId: "<3f1c2a9e-8b7d-4d61-9a63-0c2f5e7b1d44@snappymails>"
            versions:
              value:
                messageIds:
                  - "<0b6f5c1e-1d2a-4f0e-9a51-6f3c2b1d7e90@snappymails>"
                  - "<9e4d3c2b-7a6f-4e5d-8c1b-2a3f4e5d6c7b@snappymails>"
                batchId: 5c6a2f0e-7d1b-4e3a-9f2c-8b1d0e4a6c11
    EmailScheduled:
      description: |
        The message(s) were scheduled (`scheduledAt` more than 10 seconds ahead). Quota is reserved now and
        released if the send is cancelled with `DELETE /v3/smtp/email/{identifier}`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/SendEmailResponse"
          examples:
            single:
              value:
                messageId: "<3f1c2a9e-8b7d-4d61-9a63-0c2f5e7b1d44@snappymails>"
            versions:
              value:
                messageIds:
                  - "<0b6f5c1e-1d2a-4f0e-9a51-6f3c2b1d7e90@snappymails>"
                  - "<9e4d3c2b-7a6f-4e5d-8c1b-2a3f4e5d6c7b@snappymails>"
                batchId: 5c6a2f0e-7d1b-4e3a-9f2c-8b1d0e4a6c11
    BadRequest:
      description: Invalid request (`invalid_parameter`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            code: invalid_parameter
            message: sender.email is missing or invalid
    Unauthorized:
      description: Missing, unknown or revoked API key (`unauthorized`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            code: unauthorized
            message: Key not found or authentication required
    QuotaExceeded:
      description: The monthly email quota is exhausted (`quota_exceeded`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            code: quota_exceeded
            message: Monthly email quota exceeded; upgrade your plan to keep sending
    Forbidden:
      description: |
        The key lacks the required permission (`permission_denied`), the brand owner has not verified
        their email (`email_not_verified`) or sending is disabled for the brand (`account_suspended`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            permission:
              value:
                code: permission_denied
                message: API key lacks permission smtp:send
            unverified:
              value:
                code: email_not_verified
                message: Verify your email address to start sending
    NotFound:
      description: Unknown template id or message id (`document_not_found`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            code: document_not_found
            message: Template ID 12 does not exist
    TooManyRequests:
      description: The brand's rate limit was exceeded (`too_many_requests`).
      headers:
        Retry-After:
          $ref: "#/components/headers/Retry-After"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            code: too_many_requests
            message: Rate limit exceeded
    InternalError:
      description: Unexpected server error (`internal_error`). Safe to retry with backoff.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            code: internal_error
            message: Internal server error

  schemas:
    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Machine-readable error code.
          examples: [invalid_parameter]
        message:
          type: string
          description: Human-readable explanation.

    EmailAddress:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        name:
          type: string

    SendEmailRequest:
      type: object
      required: [sender]
      description: |
        Brevo v3 payload. `to` is required unless every `messageVersions` entry has its own `to`.
        Either `htmlContent`, `textContent` or `templateId` must be provided, and a subject must come
        from `subject` or the template.
      properties:
        sender:
          $ref: "#/components/schemas/EmailAddress"
        to:
          type: array
          items:
            $ref: "#/components/schemas/EmailAddress"
        cc:
          type: array
          items:
            $ref: "#/components/schemas/EmailAddress"
        bcc:
          type: array
          items:
            $ref: "#/components/schemas/EmailAddress"
        replyTo:
          $ref: "#/components/schemas/EmailAddress"
        subject:
          type: string
          description: Subject line; may contain template tags.
        htmlContent:
          type: string
          description: HTML body rendered with the template language.
        textContent:
          type: string
          description: Plain-text body rendered with the template language.
        templateId:
          type: integer
          description: Numeric id of a saved, active template (see GET /v3/smtp/templates).
        params:
          type: object
          additionalProperties: true
          description: Values available as `{{ params.KEY }}`.
        headers:
          type: object
          additionalProperties:
            type: [string, number, boolean]
          description: Custom email headers; values are sent as strings.
        tags:
          type: array
          items:
            type: string
        scheduledAt:
          type: string
          format: date-time
          description: |
            RFC 3339 timestamp with offset, up to 72 hours ahead (`400` beyond). Less than 10 seconds ahead
            (or in the past) sends immediately. A scheduled request returns `202`.
        attachment:
          type: array
          maxItems: 10
          description: |
            Files attached to every message (and every `messageVersions` entry). At most 10 files, 5 MB per
            file, 10 MB total (decoded). Executable and script types are rejected.
          items:
            $ref: "#/components/schemas/Attachment"
        messageVersions:
          type: array
          maxItems: 1000
          items:
            $ref: "#/components/schemas/MessageVersion"

    MessageVersion:
      type: object
      required: [to]
      description: Overrides the top-level fields for one message. `params` are merged over the top-level params.
      properties:
        to:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/EmailAddress"
        cc:
          type: array
          items:
            $ref: "#/components/schemas/EmailAddress"
        bcc:
          type: array
          items:
            $ref: "#/components/schemas/EmailAddress"
        replyTo:
          $ref: "#/components/schemas/EmailAddress"
        params:
          type: object
          additionalProperties: true
        subject:
          type: string
        htmlContent:
          type: string
        textContent:
          type: string

    SendEmailResponse:
      oneOf:
        - type: object
          required: [messageId]
          properties:
            messageId:
              type: string
              examples: ["<3f1c2a9e-8b7d-4d61-9a63-0c2f5e7b1d44@snappymails>"]
        - type: object
          required: [messageIds]
          properties:
            messageIds:
              type: array
              items:
                type: string
            batchId:
              type: string
              format: uuid
              description: Identifies the batch; use it to cancel or query every message of the request.

    Attachment:
      type: object
      description: |
        Either `content` (standard base64) with a `name`, or a `url` (`name` optional, defaults to the last path
        segment). URL attachments are downloaded once at request time (https only, 10 s timeout, up to 5
        redirects, private and internal addresses refused). Blocked extensions: exe com bat cmd scr pif msi msp
        msc vbs vbe js jse jar ps1 psm1 wsf wsh hta cpl reg dll sys lnk inf scf app sh apk dmg iso, and any
        content starting with an executable signature (PE, ELF, Mach-O).
      properties:
        content:
          type: string
          contentEncoding: base64
          description: Base64-encoded file content (max 5 MB decoded).
        url:
          type: string
          format: uri
          description: Absolute https URL to download the file from.
        name:
          type: string
          description: File name shown to the recipient. Required with `content`.
      oneOf:
        - required: [content, name]
        - required: [url]

    ScheduledEmailStatus:
      type: object
      required: [scheduledAt, createdAt, status]
      properties:
        scheduledAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        status:
          type: string
          enum: [queued, inProgress, processed, cancelled]
          description: "`queued` = waiting for `scheduledAt`."

    ScheduledBatchStatus:
      type: object
      required: [count, batches]
      properties:
        count:
          type: integer
        batches:
          type: array
          items:
            $ref: "#/components/schemas/ScheduledEmailStatus"

    Template:
      type: object
      required: [id, name, subject, isActive, createdAt, modifiedAt]
      properties:
        id:
          type: integer
          description: Brevo-style numeric template id, used as `templateId`.
        name:
          type: string
        subject:
          type: string
        isActive:
          type: boolean
        createdAt:
          type: string
          format: date-time
        modifiedAt:
          type: string
          format: date-time

    TemplateList:
      type: object
      required: [count, templates]
      properties:
        count:
          type: integer
        templates:
          type: array
          items:
            $ref: "#/components/schemas/Template"

    BrevoContact:
      type: object
      required: [id, email, emailBlacklisted, smsBlacklisted, createdAt, modifiedAt, listIds, attributes]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        emailBlacklisted:
          type: boolean
        smsBlacklisted:
          type: boolean
        createdAt:
          type: string
          format: date-time
        modifiedAt:
          type: string
          format: date-time
        listIds:
          type: array
          items:
            type: string
            format: uuid
        attributes:
          type: object
          additionalProperties: true

    BrevoContactList:
      type: object
      required: [count, contacts]
      properties:
        count:
          type: integer
        contacts:
          type: array
          items:
            $ref: "#/components/schemas/BrevoContact"

    CreateContactRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        attributes:
          type: object
          additionalProperties: true
          description: Custom contact attributes (normalised to uppercase keys).
        listIds:
          type: array
          items:
            type: string
            format: uuid
        updateEnabled:
          type: boolean
          default: false
          description: Upsert existing contact when true.
        emailBlacklisted:
          type: boolean

    UpdateContactRequest:
      type: object
      properties:
        attributes:
          type: object
          additionalProperties: true
        emailBlacklisted:
          type: boolean
        listIds:
          type: array
          items:
            type: string
            format: uuid
        unlinkListIds:
          type: array
          items:
            type: string
            format: uuid

    BrevoListCollection:
      type: object
      required: [count, lists]
      properties:
        count:
          type: integer
        lists:
          type: array
          items:
            type: object
            required: [id, name, totalSubscribers, optInType]
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              totalSubscribers:
                type: integer
              optInType:
                type: string
                enum: [SINGLE, DOUBLE]

    ListContactsMutation:
      type: object
      properties:
        emails:
          type: array
          items:
            type: string
            format: email
        ids:
          type: array
          items:
            type: string
            format: uuid

    ListContactsMutationResult:
      type: object
      required: [contacts]
      properties:
        contacts:
          type: object
          required: [success, failure]
          properties:
            success:
              type: array
              items:
                type: string
            failure:
              type: array
              items:
                type: string

    TrackEventRequest:
      type: object
      required: [event_name]
      description: Either top-level `email` or `identifiers.email_id` (or `identifiers.contact_id`) is required.
      properties:
        event_name:
          type: string
          examples: [cart_abandoned]
        email:
          type: string
          format: email
        identifiers:
          type: object
          properties:
            email_id:
              type: string
              format: email
            contact_id:
              type: string
              format: uuid
        contact_properties:
          type: object
          additionalProperties: true
        attributes:
          type: object
          additionalProperties: true
        event_properties:
          type: object
          additionalProperties: true
        properties:
          type: object
          additionalProperties: true
        event_date:
          type: string
          format: date-time
        occurred_at:
          type: string
          format: date-time

    TrackEventResponse:
      type: object
      required: [id, workflows_triggered, event]
      properties:
        id:
          type: string
          format: uuid
        workflows_triggered:
          type: integer
        event:
          type: object
          required: [id, email, event_name, properties, occurred_at, created_at]
          properties:
            id:
              type: string
              format: uuid
            contact_id:
              type: [string, "null"]
              format: uuid
            email:
              type: string
              format: email
            event_name:
              type: string
            properties:
              type: object
              additionalProperties: true
            occurred_at:
              type: string
              format: date-time
            created_at:
              type: string
              format: date-time

    WhatsAppTemplateParameters:
      type: object
      description: Values for the template variables, in order, per component.
      properties:
        header:
          type: array
          items:
            type: string
        body:
          type: array
          items:
            type: string
        buttons:
          type: array
          items:
            type: string

    WhatsAppSendRequest:
      type: object
      required: [to, type]
      properties:
        to:
          type: string
          description: Recipient phone number in E.164 format.
          pattern: '^\+[1-9][0-9]{6,14}$'
          examples: ["+60123456789"]
        type:
          type: string
          enum: [template, text]
        template:
          type: object
          required: [name, language]
          description: Required when `type` is `template`.
          properties:
            name:
              type: string
              examples: [order_update]
            language:
              type: string
              examples: [en_US]
            parameters:
              $ref: "#/components/schemas/WhatsAppTemplateParameters"
        text:
          type: object
          required: [body]
          description: Required when `type` is `text`. Only allowed inside the 24-hour customer service window.
          properties:
            body:
              type: string
              maxLength: 4096
            preview_url:
              type: boolean
              default: false

    WhatsAppSendResponse:
      type: object
      required: [messageId, status]
      properties:
        messageId:
          type: string
          format: uuid
        status:
          type: string
          const: QUEUED

    WaMessage:
      type: object
      required: [id, direction, phone, type, status, source, created_at]
      properties:
        id:
          type: string
          format: uuid
        direction:
          type: string
          enum: [OUTBOUND, INBOUND]
        wa_message_id:
          type: [string, "null"]
          description: Meta message id (wamid).
        phone:
          type: string
          description: Customer phone number (E.164).
        contact_id:
          type: [string, "null"]
          format: uuid
        contact_name:
          type: [string, "null"]
        campaign_id:
          type: [string, "null"]
          format: uuid
        type:
          type: string
          enum: [text, template, image, document, audio, video, location, interactive, reaction, unknown]
        text:
          type: [string, "null"]
        template:
          oneOf:
            - type: "null"
            - type: object
              properties:
                name:
                  type: string
                language:
                  type: string
                parameters:
                  $ref: "#/components/schemas/WhatsAppTemplateParameters"
        media:
          oneOf:
            - type: "null"
            - type: object
              properties:
                id:
                  type: string
                mime_type:
                  type: string
                caption:
                  type: string
        status:
          type: string
          enum: [QUEUED, SENT, DELIVERED, READ, FAILED, RECEIVED]
          description: Furthest stage reached.
        error:
          oneOf:
            - type: "null"
            - type: object
              properties:
                code:
                  type: integer
                  examples: [131047]
                title:
                  type: string
        source:
          type: string
          enum: [api, portal, campaign, inbox]
        created_at:
          type: string
          format: date-time
        sent_at:
          type: [string, "null"]
          format: date-time
        delivered_at:
          type: [string, "null"]
          format: date-time
        read_at:
          type: [string, "null"]
          format: date-time
        failed_at:
          type: [string, "null"]
          format: date-time

    WebhookEventType:
      type: string
      enum:
        - email.sent
        - email.delivered
        - email.opened
        - email.clicked
        - email.soft_bounce
        - email.hard_bounce
        - email.complaint
        - email.unsubscribed
        - email.blocked
        - whatsapp.sent
        - whatsapp.delivered
        - whatsapp.read
        - whatsapp.failed
        - whatsapp.inbound_message
        - contact.subscribed
        - contact.unsubscribed

    WebhookEvent:
      type: object
      description: |
        Envelope of every webhook delivery. `id` is stable across retries and manual resends; use it to
        deduplicate. Verify the `SnappyMails-Signature` header before trusting the body.
      required: [id, type, created_at, brand_id, test, data]
      properties:
        id:
          type: string
          format: uuid
        type:
          $ref: "#/components/schemas/WebhookEventType"
        created_at:
          type: string
          format: date-time
        brand_id:
          type: string
          format: uuid
        test:
          type: boolean
          description: "`true` only for deliveries created with Send test event."
        data:
          oneOf:
            - $ref: "#/components/schemas/WebhookEmailData"
            - $ref: "#/components/schemas/WebhookWhatsAppData"
            - $ref: "#/components/schemas/WebhookContactData"

    WebhookEmailData:
      type: object
      description: Brevo transactional-webhook compatible field names.
      required: [event, email, id, message-id, date, ts, ts_event, ts_epoch, channel]
      properties:
        event:
          type: string
          enum: [request, delivered, opened, click, soft_bounce, hard_bounce, spam, unsubscribed, blocked]
        email:
          type: string
          format: email
        id:
          type: string
          description: SnappyMails event id (UUID string).
        message-id:
          type: string
          examples: ["<5d0e6a1c-2f3b-4c4d-8e9f-0a1b2c3d4e5f@snappymails>"]
        date:
          type: string
          description: UTC `YYYY-MM-DD HH:MM:SS`.
        ts:
          type: integer
          description: Unix seconds.
        ts_event:
          type: integer
          description: Unix seconds.
        ts_epoch:
          type: integer
          description: Unix milliseconds.
        channel:
          type: string
          enum: [transactional, campaign, autoresponder]
        subject:
          type: string
        sender:
          type: string
        tag:
          type: string
          description: First tag (transactional mail sent with `tags`).
        tags:
          type: array
          items:
            type: string
        campaign_id:
          type: [string, "null"]
        link:
          type: string
          description: "`email.clicked` only."
        ip:
          type: string
          description: "`email.opened` and `email.clicked`."
        user_agent:
          type: string
          description: "`email.opened` and `email.clicked`."
        reason:
          type: string
          description: Bounce diagnostic, or `suppressed` for `email.blocked`.
        bounce_type:
          type: string
        bounce_sub_type:
          type: string
        method:
          type: string
          enum: [link, one_click, complaint]
          description: "`email.unsubscribed` only."
        feedback_type:
          type: string
          description: "`email.complaint` only."

    WebhookWhatsAppData:
      type: object
      required: [event, date, ts, ts_epoch]
      properties:
        event:
          type: string
          enum: [sent, delivered, read, failed, inbound_message]
        message_id:
          type: string
        wamid:
          type: string
        phone:
          type: string
        status:
          type: string
        campaign_id:
          type: [string, "null"]
        pricing_category:
          type: [string, "null"]
        error:
          oneOf:
            - type: "null"
            - type: object
              properties:
                code:
                  type: integer
                  examples: [131026]
                title:
                  type: string
        type:
          type: string
          description: "`whatsapp.inbound_message`: text, image, document, interactive, ..."
        text:
          type: [string, "null"]
        contact_name:
          type: [string, "null"]
        contact_id:
          type: [string, "null"]
        media:
          oneOf:
            - type: "null"
            - type: object
              properties:
                id:
                  type: string
                mime_type:
                  type: string
                caption:
                  type: string
                filename:
                  type: string
        date:
          type: string
        ts:
          type: integer
        ts_epoch:
          type: integer

    WebhookContactData:
      type: object
      required: [event, email, contact_id, date, ts]
      properties:
        event:
          type: string
          enum: [subscribed, unsubscribed]
        email:
          type: string
          format: email
        contact_id:
          type: string
        list_id:
          type: [string, "null"]
        source:
          type: string
          enum: [list, resubscribe]
          description: "`contact.subscribed` only."
        date:
          type: string
        ts:
          type: integer

webhooks:
  event:
    post:
      tags: [Webhooks]
      operationId: webhookEvent
      summary: Event notification
      description: |
        SnappyMails POSTs one event per request to every subscribed endpoint. Respond with any `2xx` within
        10 seconds; redirects are not followed. Failed deliveries are retried with exponential backoff
        (30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h, 8 h: 10 attempts over about 24 hours).

        `SnappyMails-Signature: t=<unix seconds>,v1=<hex>`: `v1` is `HMAC-SHA256(secret, "<t>.<raw body>")`.
        During a secret rotation several `v1` values are sent; accept the request when any matches. Reject
        requests whose `t` is more than 5 minutes from your clock. See /docs/webhooks.
      security: []
      parameters:
        - name: SnappyMails-Signature
          in: header
          required: true
          schema:
            type: string
            examples: ["t=1792228364,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"]
        - name: SnappyMails-Event-Id
          in: header
          required: true
          schema:
            type: string
            format: uuid
        - name: SnappyMails-Event-Type
          in: header
          required: true
          schema:
            $ref: "#/components/schemas/WebhookEventType"
        - name: SnappyMails-Delivery-Id
          in: header
          required: true
          schema:
            type: string
        - name: SnappyMails-Attempt
          in: header
          required: true
          schema:
            type: integer
            minimum: 1
        - name: User-Agent
          in: header
          schema:
            type: string
            examples: [SnappyMails-Webhooks/1.0]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
            examples:
              emailClicked:
                summary: email.clicked
                value:
                  id: 0b9d3c7e-5a51-4a8e-9f0e-1b8f6f0d2a11
                  type: email.clicked
                  created_at: "2026-10-17T09:12:44Z"
                  brand_id: 4f1c8a2e-9b3d-4e6f-8a1c-2d3e4f5a6b7c
                  test: false
                  data:
                    event: click
                    email: jane@example.com
                    id: 0b9d3c7e-5a51-4a8e-9f0e-1b8f6f0d2a11
                    message-id: "<5d0e6a1c-2f3b-4c4d-8e9f-0a1b2c3d4e5f@snappymails>"
                    date: "2026-10-17 09:12:44"
                    ts: 1792228364
                    ts_event: 1792228364
                    ts_epoch: 1792228364123
                    subject: Your order A-77
                    tag: order
                    tags: [order, eu]
                    link: https://shop.example/track/A-77
                    channel: transactional
                    campaign_id: null
                    sender: orders@shop.example
              whatsappDelivered:
                summary: whatsapp.delivered
                value:
                  id: 6a2b1c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d
                  type: whatsapp.delivered
                  created_at: "2026-10-17T09:12:44Z"
                  brand_id: 4f1c8a2e-9b3d-4e6f-8a1c-2d3e4f5a6b7c
                  test: false
                  data:
                    event: delivered
                    message_id: 8a0c4d2e-1f3a-4b5c-9d6e-7f8a9b0c1d2e
                    wamid: wamid.HBgMNjAxMjM0NTY3ODkVAgARGBI
                    phone: "+60123456789"
                    status: DELIVERED
                    campaign_id: null
                    pricing_category: utility
                    error: null
                    date: "2026-10-17 09:12:44"
                    ts: 1792228364
                    ts_epoch: 1792228364000
              contactSubscribed:
                summary: contact.subscribed
                value:
                  id: 9c8b7a6f-5e4d-4c3b-8a2f-1e0d9c8b7a6f
                  type: contact.subscribed
                  created_at: "2026-10-17T09:12:44Z"
                  brand_id: 4f1c8a2e-9b3d-4e6f-8a1c-2d3e4f5a6b7c
                  test: false
                  data:
                    event: subscribed
                    email: jane@example.com
                    contact_id: c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f
                    list_id: 7b1a2c3d-d5e6-4f7a-8b9c-0d1e2f3a4b5c
                    source: list
                    date: "2026-10-17 09:12:44"
                    ts: 1792228364
      responses:
        "2XX":
          description: Acknowledged. Any other status (or a timeout) is retried.
