openapi: 3.1.0
info:
  title: Sendbaze API
  version: "1.0"
  description: |
    Transactional email and SMS for Africa. Send from your own domain through Amazon SES in Cape Town.
    Every send returns an id immediately; delivery is reported by webhook and in the dashboard logs.
  contact:
    name: Sendbaze support
    email: support@sendbaze.com
    url: https://www.sendbaze.com
servers:
  - url: https://api.sendbaze.com
security:
  - bearerAuth: []
tags:
  - name: Emails
  - name: Templates
  - name: Domains
  - name: Suppressions
  - name: Webhooks
  - name: SMS
  - name: Usage

paths:
  /v1/emails:
    post:
      tags: [Emails]
      summary: Send an email
      operationId: sendEmail
      description: |
        Validates the request, stores the message, queues it and returns the id. Requires the `emails:send` scope
        and, for live keys, a verified sending domain, or the shared test address `onboarding@sendbaze.com`, which
        delivers to your organisation's own addresses only (100 emails per organisation in total). Add an
        `Idempotency-Key` header to make retries safe.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendEmailRequest"
            examples:
              simple:
                summary: HTML and text
                value:
                  from: "Yourshop <orders@yourshop.co.za>"
                  to: ["thandi@example.com"]
                  replyTo: "help@yourshop.co.za"
                  subject: "Your order #1042 has shipped"
                  html: "<p>Tracking: <b>CG123456789ZA</b></p>"
                  text: "Tracking: CG123456789ZA"
                  tags: ["order-shipped"]
              template:
                summary: With a template
                value:
                  from: "orders@yourshop.co.za"
                  to: "thandi@example.com"
                  templateId: "tpl_01j9x7k2r4m8q3v5n6b7c8d9e0"
                  variables: { name: "Thandi", order_id: 1042, eta: "Friday" }
              attachment:
                summary: With an attachment and an inline image
                value:
                  from: "orders@yourshop.co.za"
                  to: "thandi@example.com"
                  subject: "Invoice 1042"
                  html: '<p>Your invoice is attached.</p><img src="cid:logo" alt="Yourshop">'
                  attachments:
                    - filename: "invoice-1042.pdf"
                      contentType: "application/pdf"
                      content: "JVBERi0xLjQK..."
                    - filename: "logo.png"
                      contentType: "image/png"
                      contentId: "logo"
                      content: "iVBORw0KGgo..."
      responses:
        "200":
          description: Queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SendEmailResponse"
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "423": { $ref: "#/components/responses/Locked" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Emails]
      summary: List emails
      operationId: listEmails
      description: |
        Your emails, newest first, cursor-paged. Items are the message object without `events`;
        retrieve one for its timeline. Pages stay stable while new email arrives. Requires `read`.
      parameters:
        - name: status
          in: query
          schema: { $ref: "#/components/schemas/MessageStatus" }
        - name: tag
          in: query
          description: Exact match on one tag.
          schema: { type: string, maxLength: 64 }
        - name: to
          in: query
          description: Substring of the recipient, case-insensitive.
          schema: { type: string, maxLength: 254 }
        - name: since
          in: query
          description: ISO 8601 date-time; emails created at or after it.
          schema: { type: string, format: date-time }
        - name: until
          in: query
          description: ISO 8601 date-time; emails created before it.
          schema: { type: string, format: date-time }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of emails
          content:
            application/json:
              schema:
                type: object
                required: [data, nextCursor]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/EmailSummary" }
                  nextCursor:
                    type: [string, "null"]
                    description: Pass as `cursor` for the next page; null on the last page.
        "422": { $ref: "#/components/responses/Unprocessable" }

  /v1/emails/batch:
    post:
      tags: [Emails]
      summary: Send a batch of emails
      operationId: sendEmailBatch
      description: |
        Up to 1,000 emails in one request. Each item is validated and queued on its own, so a bad item fails alone.
        Results come back in input order. `Idempotency-Key` applies to the whole batch. Requires `emails:send`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [emails]
              properties:
                emails:
                  type: array
                  minItems: 1
                  maxItems: 1000
                  items: { $ref: "#/components/schemas/SendEmailRequest" }
            example:
              emails:
                - { from: "orders@yourshop.co.za", to: "a@example.com", subject: "Hi", text: "..." }
                - { from: "orders@yourshop.co.za", to: "not-an-email", subject: "Hi", text: "..." }
      responses:
        "200":
          description: Per-item results, in input order
          content:
            application/json:
              schema:
                type: object
                required: [results]
                properties:
                  results:
                    type: array
                    items:
                      $ref: "#/components/schemas/BatchItemResult"
              example:
                results:
                  - { index: 0, ok: true, data: { id: "em_01j9x7k2r4m8q3v5n6b7c8d9e0", status: "queued", mode: "live" } }
                  - index: 1
                    ok: false
                    error:
                      code: validation_error
                      message: The request has invalid fields.
                      fields: { to: ["must be a valid email address"] }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "423": { $ref: "#/components/responses/Locked" }

  /v1/emails/{id}:
    get:
      tags: [Emails]
      summary: Retrieve an email
      operationId: getEmail
      description: The message object with its full event timeline and attachment list. Requires `read`.
      parameters:
        - $ref: "#/components/parameters/EmailId"
      responses:
        "200":
          description: The email
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EmailMessage" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/templates:
    post:
      tags: [Templates]
      summary: Create a template
      operationId: createTemplate
      description: |
        Creates version 1. The response lists the `{{variables}}` found in the subject, html and text.
        A duplicate name is a 409. Requires `emails:send`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateTemplateRequest" }
            example:
              name: order-shipped
              subject: "Your order {{order_id}} has shipped"
              html: "<p>Hi {{name}},</p><p>Order <b>{{order_id}}</b> arrives by {{eta}}.</p>"
              text: "Hi {{name}}, order {{order_id}} arrives by {{eta}}."
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Template" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
    get:
      tags: [Templates]
      summary: List templates
      operationId: listTemplates
      description: Every template of the organisation, most recently changed first. Requires `read`.
      responses:
        "200":
          description: Templates
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Template" }

  /v1/templates/{id}:
    get:
      tags: [Templates]
      summary: Retrieve a template
      operationId: getTemplate
      description: The current content and version. Requires `read`.
      parameters: [{ $ref: "#/components/parameters/TemplateId" }]
      responses:
        "200":
          description: The template
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Template" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Templates]
      summary: Update a template
      operationId: updateTemplate
      description: |
        Any of `name`, `subject`, `html`, `text`. Setting `html` or `text` to null removes that part; one must remain.
        A content change creates a new version; a rename does not. Requires `emails:send`.
      parameters: [{ $ref: "#/components/parameters/TemplateId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateTemplateRequest" }
            example:
              subject: "Order {{order_id}} is on its way"
      responses:
        "200":
          description: The updated template
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Template" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
    delete:
      tags: [Templates]
      summary: Delete a template
      operationId: deleteTemplate
      description: Removes the template and its versions. Email already sent is unaffected; later sends naming the id fail with 422. Requires `emails:send`.
      parameters: [{ $ref: "#/components/parameters/TemplateId" }]
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Deleted" }
              example: { id: "tpl_01j9x7k2r4m8q3v5n6b7c8d9e0", deleted: true }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/templates/{id}/render:
    post:
      tags: [Templates]
      summary: Preview a render
      operationId: renderTemplate
      description: |
        The subject, html and text a send with these variables would produce. Placeholders you leave out are listed
        under `missing` and left in place, so partial previews still render. Requires `read`.
      parameters: [{ $ref: "#/components/parameters/TemplateId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                variables: { $ref: "#/components/schemas/TemplateVariables" }
            example:
              variables: { name: "Thandi <script>", order_id: 1042 }
      responses:
        "200":
          description: The render
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RenderedTemplate" }
              example:
                version: 1
                subject: "Your order 1042 has shipped"
                html: "<p>Hi Thandi &lt;script&gt;,</p><p>Order <b>1042</b> arrives by {{eta}}.</p>"
                text: "Hi Thandi <script>, order 1042 arrives by {{eta}}."
                missing: ["eta"]
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/templates/{id}/versions:
    get:
      tags: [Templates]
      summary: List template versions
      operationId: listTemplateVersions
      description: Every version, newest first, with its full content. Requires `read`.
      parameters: [{ $ref: "#/components/parameters/TemplateId" }]
      responses:
        "200":
          description: Versions
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/TemplateVersion" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains:
    post:
      tags: [Domains]
      summary: Add a domain
      operationId: addDomain
      description: |
        Creates the sending identity and returns the DNS records to publish: three DKIM CNAMEs, an MX and an SPF TXT
        for the custom return-path host, and a suggested DMARC record. Adding a domain you already have returns it
        with 200. Requires `domains:write`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateDomainRequest" }
            example: { name: "yourshop.co.za" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Domain" }
        "200":
          description: Already added
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Domain" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    get:
      tags: [Domains]
      summary: List domains
      operationId: listDomains
      description: Newest first. Statuses are re-checked every five minutes while pending and daily once verified. Requires `read`.
      responses:
        "200":
          description: Domains
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Domain" }

  /v1/domains/{id}:
    get:
      tags: [Domains]
      summary: Retrieve a domain
      operationId: getDomain
      parameters: [{ $ref: "#/components/parameters/DomainId" }]
      responses:
        "200":
          description: The domain with per-record status
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Domain" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Domains]
      summary: Delete a domain
      operationId: deleteDomain
      description: Removes the identity. Sends from the domain fail with `domain_not_verified` from the next request. Requires `domains:write`.
      parameters: [{ $ref: "#/components/parameters/DomainId" }]
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{id}/verify:
    post:
      tags: [Domains]
      summary: Check a domain now
      operationId: verifyDomain
      description: Asks SES and public DNS immediately instead of waiting for the next poll, and returns the updated domain. Requires `domains:write`.
      parameters: [{ $ref: "#/components/parameters/DomainId" }]
      responses:
        "200":
          description: The domain after the check
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Domain" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/suppressions:
    get:
      tags: [Suppressions]
      summary: List suppressions
      operationId: listSuppressions
      description: Newest first, cursor-paged. Requires `read`.
      parameters:
        - name: email
          in: query
          description: Substring match on the address.
          schema: { type: string, maxLength: 254 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of suppressions
          content:
            application/json:
              schema:
                type: object
                required: [data, nextCursor]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Suppression" }
                  nextCursor: { type: [string, "null"] }
    post:
      tags: [Suppressions]
      summary: Add a suppression
      operationId: addSuppression
      description: 201 when created, 200 when the address was already listed. Requires `emails:send`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateSuppressionRequest" }
            example: { email: "person@example.co.za", reason: "manual", note: "Asked to be removed" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Suppression" }
        "200":
          description: Already listed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Suppression" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /v1/suppressions/{id}:
    delete:
      tags: [Suppressions]
      summary: Remove a suppression
      operationId: removeSuppression
      description: Removes one of your own entries so the address can receive email again. Global hard-bounce entries cannot be removed. Requires `emails:send`.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, example: sup_01j9x7k2r4m8q3v5n6b7c8d9e0 }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/webhooks:
    post:
      tags: [Webhooks]
      summary: Create a webhook endpoint
      operationId: createWebhook
      description: The response includes `secret` exactly once; it is stored encrypted and never returned again. Requires `webhooks:write`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateWebhookRequest" }
            example:
              url: "https://api.yourshop.co.za/webhooks/sendbaze"
              description: "Production CRM sync"
              events: ["email.bounced", "email.complained", "email.unsubscribed"]
      responses:
        "201":
          description: Created, with the signing secret
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreatedWebhookEndpoint" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      operationId: listWebhooks
      description: Every endpoint of the organisation, newest first. Requires `read`.
      responses:
        "200":
          description: Endpoints
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookEndpoint" }

  /v1/webhooks/{id}:
    get:
      tags: [Webhooks]
      summary: Retrieve a webhook endpoint
      operationId: getWebhook
      parameters: [{ $ref: "#/components/parameters/WebhookId" }]
      responses:
        "200":
          description: The endpoint
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Webhooks]
      summary: Update a webhook endpoint
      operationId: updateWebhook
      description: Change the URL, description, event filter or enabled flag. Requires `webhooks:write`.
      parameters: [{ $ref: "#/components/parameters/WebhookId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateWebhookRequest" }
            example: { enabled: true }
      responses:
        "200":
          description: The updated endpoint
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook endpoint
      operationId: deleteWebhook
      description: Removes the endpoint and its delivery log. Requires `webhooks:write`.
      parameters: [{ $ref: "#/components/parameters/WebhookId" }]
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Deleted" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/webhooks/{id}/test:
    post:
      tags: [Webhooks]
      summary: Send a test event
      operationId: testWebhook
      description: Queues a `webhook.test` event to this endpoint regardless of its filter. Requires `webhooks:write`.
      parameters: [{ $ref: "#/components/parameters/WebhookId" }]
      responses:
        "200":
          description: Queued
          content:
            application/json:
              schema:
                type: object
                required: [eventId]
                properties:
                  eventId: { type: string, example: evt_01j9x7m3p5q7r9s1t3u5v7w9x1 }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/webhooks/{id}/deliveries:
    get:
      tags: [Webhooks]
      summary: List deliveries
      operationId: listWebhookDeliveries
      description: The delivery log, newest first, cursor-paged. Requires `read`.
      parameters:
        - $ref: "#/components/parameters/WebhookId"
        - name: status
          in: query
          schema: { type: string, enum: [pending, succeeded, failed] }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of deliveries
          content:
            application/json:
              schema:
                type: object
                required: [data, nextCursor]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookDelivery" }
                  nextCursor: { type: [string, "null"] }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/webhooks/{id}/deliveries/{deliveryId}/replay:
    post:
      tags: [Webhooks]
      summary: Replay a delivery
      operationId: replayWebhookDelivery
      description: Sends the event again as a new delivery with a fresh signature. Requires `webhooks:write`.
      parameters:
        - $ref: "#/components/parameters/WebhookId"
        - name: deliveryId
          in: path
          required: true
          schema: { type: string, example: whd_01j9x7m3p5q7r9s1t3u5v7w9x1 }
      responses:
        "201":
          description: The new delivery
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookDelivery" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sms:
    post:
      tags: [SMS]
      summary: Send an SMS
      operationId: sendSms
      description: |
        Counts segments, checks credits, applies the compliance rules for the message class, debits credits and
        queues the message. Marketing SMS outside 08:00 to 20:00 SAST (or on Sundays and public holidays) is held
        and sent at 08:00 on the next permitted day with status `scheduled`. Requires `sms:send`.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SendSmsRequest" }
            example:
              to: "+27821234567"
              body: "Your OTP is 482 913. It expires in 5 minutes."
              class: transactional
      responses:
        "200":
          description: Queued or scheduled
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SendSmsResponse" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "423": { $ref: "#/components/responses/Locked" }

  /v1/sms/batch:
    post:
      tags: [SMS]
      summary: Send a batch of SMS
      operationId: sendSmsBatch
      description: Up to 1,000 messages with per-item results. If credits run out part-way, the remaining items fail with `insufficient_credits` and the earlier ones still send. Requires `sms:send`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [messages]
              properties:
                messages:
                  type: array
                  minItems: 1
                  maxItems: 1000
                  items: { $ref: "#/components/schemas/SendSmsRequest" }
      responses:
        "200":
          description: Per-item results
          content:
            application/json:
              schema:
                type: object
                required: [results]
                properties:
                  results:
                    type: array
                    items: { $ref: "#/components/schemas/BatchSmsItemResult" }

  /v1/sms/{id}:
    get:
      tags: [SMS]
      summary: Retrieve an SMS
      operationId: getSms
      description: The message with `segments`, `credits`, `scheduledAt` and the event timeline. Requires `read`.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, example: sm_01j9x7k2r4m8q3v5n6b7c8d9e0 }
      responses:
        "200":
          description: The SMS
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SmsMessage" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sms/coverage:
    get:
      tags: [SMS]
      summary: SMS coverage
      operationId: getSmsCoverage
      description: |
        Every country Sendbaze sends SMS to, with the route that carries it, the price per segment, whether the
        route is live (`available`) and whether your organisation has enabled it (`enabled`). Sends to a country
        that is not available answer `422 destination_not_supported`; to one your organisation has not enabled,
        `403 destination_not_enabled` with a `settingsUrl`. Requires `read`.
      responses:
        "200":
          description: Coverage
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SmsCoverage" }

  /v1/usage:
    get:
      tags: [Usage]
      summary: Retrieve usage
      operationId: getUsage
      description: Your plan, the current billing period, how much of each allowance is used and remaining, prepaid SMS credits and domains in use. Requires `read`.
      responses:
        "200":
          description: Usage for the current period
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Usage" }
              example:
                plan: { id: starter, name: Starter, priceRand: 249 }
                period: { start: "2026-10-23T18:00:00.000Z", end: "2026-11-23T18:00:00.000Z" }
                emails: { used: 4210, included: 10000, remaining: 5790 }
                sms: { used: 300, included: 300, remaining: 0, credits: 1000 }
                domains: { used: 1, included: 2 }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: An API key from the dashboard. `sk_live_…` sends; `sk_test_…` validates and returns ids without sending.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Up to 255 characters. The same key and body return the same response for 24 hours.
      schema: { type: string, maxLength: 255 }
    Cursor:
      name: cursor
      in: query
      description: The `nextCursor` of the previous page.
      schema: { type: string }
    EmailId:
      name: id
      in: path
      required: true
      schema: { type: string, example: em_01j9x7k2r4m8q3v5n6b7c8d9e0 }
    TemplateId:
      name: id
      in: path
      required: true
      schema: { type: string, example: tpl_01j9x7k2r4m8q3v5n6b7c8d9e0 }
    DomainId:
      name: id
      in: path
      required: true
      schema: { type: string, example: dom_01j9x7k2r4m8q3v5n6b7c8d9e0 }
    WebhookId:
      name: id
      in: path
      required: true
      schema: { type: string, example: whk_01j9x7k2r4m8q3v5n6b7c8d9e0 }

  responses:
    NotFound:
      description: No such resource in your organisation
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example: { error: { code: not_found, message: "No email with id em_x.", docs: "https://docs.sendbaze.com/errors#not_found" } }
    Unprocessable:
      description: Validation failed, a recipient is suppressed, or an SMS destination is not supported (destination_not_supported)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example:
            error:
              code: validation_error
              message: The request has invalid fields.
              docs: "https://docs.sendbaze.com/errors#validation_error"
              fields: { "to.1": ["must be a valid email address"] }
    Forbidden:
      description: Insufficient scope, unverified domain, suspended organisation, or an SMS destination the organisation has not enabled (destination_not_enabled)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example: { error: { code: domain_not_verified, message: "yourshop.co.za is not a verified sending domain of this organisation.", docs: "https://docs.sendbaze.com/errors#domain_not_verified" } }
    PaymentRequired:
      description: Allowance used up, not enough credits, or plan limit reached
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    PayloadTooLarge:
      description: Attachments over 3 MB decoded in total
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
          example: { error: { code: payload_too_large, message: "Attachments may be at most 3 MB in total.", docs: "https://docs.sendbaze.com/errors#payload_too_large" } }
    Locked:
      description: Sending is paused for this organisation
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    Conflict:
      description: Idempotency conflict or duplicate name
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    RateLimited:
      description: More than 100 requests per second, or the daily warm-up cap (warmup_limit_reached); see Retry-After
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
    ServiceUnavailable:
      description: A dependency is unavailable; nothing was changed
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }

  schemas:
    MessageClass:
      type: string
      enum: [transactional, marketing]
    MessageStatus:
      type: string
      enum: [queued, sent, delivered, bounced, complained, failed, expired, rejected]
    Recipients:
      description: One address or up to 50.
      oneOf:
        - type: string
          format: email
        - type: array
          maxItems: 50
          items: { type: string, format: email }
    TemplateVariables:
      type: object
      description: String, number or boolean values keyed by placeholder name.
      additionalProperties:
        oneOf: [{ type: string }, { type: number }, { type: boolean }]
    EmailAttachment:
      type: object
      required: [filename, content]
      properties:
        filename:
          type: string
          maxLength: 255
          description: Shown to the recipient. Path separators are refused.
        content:
          type: string
          description: The file, base64-encoded.
        contentType:
          type: string
          description: MIME type. Defaults to application/octet-stream.
          example: application/pdf
        contentId:
          type: string
          description: Set to embed the file inline and reference it from the HTML as `<img src="cid:{contentId}">`.
    SendEmailRequest:
      type: object
      required: [from, to]
      properties:
        from:
          type: string
          description: A bare address or `Name <address>` on a verified domain, or the shared test address `onboarding@sendbaze.com` (own addresses only).
          example: "Yourshop <orders@yourshop.co.za>"
        to: { $ref: "#/components/schemas/Recipients" }
        cc: { $ref: "#/components/schemas/Recipients" }
        bcc: { $ref: "#/components/schemas/Recipients" }
        replyTo: { type: string, format: email }
        subject:
          type: string
          maxLength: 998
          description: Required unless `templateId` is set, in which case it overrides the template's subject.
        html:
          type: string
          description: HTML body, up to 2 MB. One of `html`, `text` or `templateId` is required.
        text:
          type: string
          description: Plain-text body, up to 2 MB.
        templateId:
          type: string
          description: Render a template (its current version) instead of passing html or text.
        variables: { $ref: "#/components/schemas/TemplateVariables" }
        attachments:
          type: array
          maxItems: 10
          description: Up to 10 files, 3 MB decoded in total.
          items: { $ref: "#/components/schemas/EmailAttachment" }
        headers:
          type: object
          description: Custom headers. Standard headers such as From, To and Subject cannot be overridden.
          additionalProperties: { type: string }
        tags:
          type: array
          maxItems: 10
          items: { type: string, maxLength: 64 }
        class:
          allOf: [{ $ref: "#/components/schemas/MessageClass" }]
          default: transactional
    SendEmailResponse:
      type: object
      required: [id, status, mode]
      properties:
        id: { type: string, example: em_01j9x7k2r4m8q3v5n6b7c8d9e0 }
        status: { type: string, enum: [queued] }
        mode:
          type: string
          enum: [live, test]
          description: "`test` when sent with an sk_test_ key: validated and stored, never delivered."
    MessageError:
      type: [object, "null"]
      required: [code, message]
      properties:
        code: { type: string, example: hard_bounce }
        message: { type: string }
    MessageEvent:
      type: object
      required: [type, occurredAt]
      properties:
        type:
          type: string
          enum: [queued, attempt_failed, sent, delivery_delayed, delivered, bounced, complained, failed, expired, rejected, unsubscribed]
        occurredAt: { type: string, format: date-time }
        detail:
          type: object
          description: Provider ids, error codes, attempt numbers; shape depends on the type.
          additionalProperties: true
    EmailAttachmentInfo:
      type: object
      required: [filename, contentType, size]
      properties:
        filename: { type: string }
        contentType: { type: string }
        size: { type: integer, description: Decoded size in bytes. }
        contentId: { type: string }
    EmailSummary:
      type: object
      required: [id, channel, class, status, to, from, subject, tags, error, createdAt, sentAt, deliveredAt]
      properties:
        id: { type: string, example: em_01j9x7k2r4m8q3v5n6b7c8d9e0 }
        channel: { type: string, enum: [email] }
        class: { $ref: "#/components/schemas/MessageClass" }
        status: { $ref: "#/components/schemas/MessageStatus" }
        to: { type: string, description: Recipients as sent, comma-separated when there were several. }
        from: { type: string }
        subject: { type: [string, "null"] }
        tags: { type: array, items: { type: string } }
        error: { $ref: "#/components/schemas/MessageError" }
        createdAt: { type: string, format: date-time }
        sentAt: { type: [string, "null"], format: date-time }
        deliveredAt: { type: [string, "null"], format: date-time }
    EmailMessage:
      allOf:
        - $ref: "#/components/schemas/EmailSummary"
        - type: object
          required: [attachments, events]
          properties:
            attachments:
              type: array
              items: { $ref: "#/components/schemas/EmailAttachmentInfo" }
            events:
              type: array
              description: The full timeline, oldest first.
              items: { $ref: "#/components/schemas/MessageEvent" }
    BatchItemResult:
      type: object
      required: [index, ok]
      properties:
        index: { type: integer }
        ok: { type: boolean }
        data: { $ref: "#/components/schemas/SendEmailResponse" }
        error: { $ref: "#/components/schemas/ErrorBody" }
    BatchSmsItemResult:
      type: object
      required: [index, ok]
      properties:
        index: { type: integer }
        ok: { type: boolean }
        data: { $ref: "#/components/schemas/SendSmsResponse" }
        error: { $ref: "#/components/schemas/ErrorBody" }
    CreateTemplateRequest:
      type: object
      required: [name, subject]
      properties:
        name:
          type: string
          maxLength: 80
          description: Unique in your organisation. Letters, digits, spaces, dots, underscores, dashes.
        subject: { type: string, maxLength: 998 }
        html: { type: string, description: One of html or text is required. }
        text: { type: string }
    UpdateTemplateRequest:
      type: object
      properties:
        name: { type: string, maxLength: 80 }
        subject: { type: string, maxLength: 998 }
        html: { type: [string, "null"], description: null removes the part. }
        text: { type: [string, "null"] }
    Template:
      type: object
      required: [id, name, subject, html, text, variables, version, createdAt, updatedAt]
      properties:
        id: { type: string, example: tpl_01j9x7k2r4m8q3v5n6b7c8d9e0 }
        name: { type: string, example: order-shipped }
        subject: { type: string }
        html: { type: [string, "null"] }
        text: { type: [string, "null"] }
        variables:
          type: array
          description: Placeholder names found in the subject, html and text.
          items: { type: string }
          example: [order_id, name, eta]
        version: { type: integer, example: 1 }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    TemplateVersion:
      type: object
      required: [id, version, subject, html, text, variables, createdBy, createdAt]
      properties:
        id: { type: string, example: tpv_01j9x7k2r4m8q3v5n6b7c8d9e0 }
        version: { type: integer }
        subject: { type: string }
        html: { type: [string, "null"] }
        text: { type: [string, "null"] }
        variables: { type: array, items: { type: string } }
        createdBy: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
    RenderedTemplate:
      type: object
      required: [version, subject, html, text, missing]
      properties:
        version: { type: integer }
        subject: { type: string }
        html: { type: [string, "null"] }
        text: { type: [string, "null"] }
        missing:
          type: array
          description: Placeholders the variables did not cover; left as `{{name}}` in the output.
          items: { type: string }
    CreateDomainRequest:
      type: object
      required: [name]
      properties:
        name: { type: string, example: yourshop.co.za }
        mailFromSubdomain:
          type: string
          default: mail
          description: Subdomain for the custom return path.
    DnsRecord:
      type: object
      required: [purpose, type, name, value, required, status, found, checkedAt]
      properties:
        purpose: { type: string, enum: [dkim, mail_from_mx, mail_from_spf, dmarc] }
        type: { type: string, enum: [CNAME, MX, TXT] }
        name: { type: string, example: abc123._domainkey.yourshop.co.za }
        value: { type: string, example: abc123.dkim.af-south-1.amazonses.com }
        required: { type: boolean, description: Sending is blocked until every required record verifies; DMARC is advice. }
        status: { type: string, enum: [pending, verified, failed] }
        found:
          type: array
          description: What public DNS currently returns for the name.
          items: { type: string }
        wildcard: { type: boolean, description: The answer comes from a wildcard record; publishing the exact record overrides it. }
        checkedAt: { type: [string, "null"], format: date-time }
    Domain:
      type: object
      required: [id, name, status, mailFromDomain, records, verifiedAt, lastCheckedAt, createdAt]
      properties:
        id: { type: string, example: dom_01j9x7k2r4m8q3v5n6b7c8d9e0 }
        name: { type: string, example: yourshop.co.za }
        status: { type: string, enum: [pending, verified, failed] }
        mailFromDomain: { type: string, example: mail.yourshop.co.za }
        records:
          type: array
          items: { $ref: "#/components/schemas/DnsRecord" }
        advice:
          type: array
          description: Deliverability findings from the last check (DMARC, apex SPF, alignment).
          items: { $ref: "#/components/schemas/DomainAdvice" }
        verifiedAt: { type: [string, "null"], format: date-time }
        lastCheckedAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
    CreateSuppressionRequest:
      type: object
      required: [email]
      properties:
        email: { type: string, format: email }
        reason:
          type: string
          enum: [manual, unsubscribe]
          default: manual
          description: manual blocks all email; unsubscribe blocks marketing only.
        note: { type: string, maxLength: 500 }
    Suppression:
      type: object
      required: [id, email, reason, messageId, note, createdAt]
      properties:
        id: { type: string, example: sup_01j9x7k2r4m8q3v5n6b7c8d9e0 }
        email: { type: string }
        reason: { type: string, enum: [hard_bounce, complaint, unsubscribe, manual] }
        messageId: { type: [string, "null"], description: The message whose bounce, complaint or unsubscribe created the entry. }
        note: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
    WebhookEventType:
      type: string
      enum:
        - email.sent
        - email.delivered
        - email.bounced
        - email.complained
        - email.failed
        - email.unsubscribed
        - sms.sent
        - sms.delivered
        - sms.failed
        - sms.opted_out
        - usage.threshold
        - credits.low
        - billing.payment_failed
    CreateWebhookRequest:
      type: object
      required: [url]
      properties:
        url: { type: string, format: uri, description: https URL that answers 2xx within 10 seconds. Private and local addresses are refused. }
        description: { type: string, maxLength: 200 }
        events:
          type: array
          description: Omit or send [] for every event.
          items: { $ref: "#/components/schemas/WebhookEventType" }
    UpdateWebhookRequest:
      type: object
      properties:
        url: { type: string, format: uri }
        description: { type: [string, "null"], maxLength: 200 }
        events:
          type: array
          items: { $ref: "#/components/schemas/WebhookEventType" }
        enabled: { type: boolean }
    WebhookEndpoint:
      type: object
      required: [id, url, description, events, enabled, disabledReason, secretPrefix, failingSince, lastSuccessAt, lastFailureAt, createdAt]
      properties:
        id: { type: string, example: whk_01j9x7k2r4m8q3v5n6b7c8d9e0 }
        url: { type: string }
        description: { type: [string, "null"] }
        events: { type: array, items: { type: string }, description: Empty means every event. }
        enabled: { type: boolean }
        disabledReason: { type: [string, "null"] }
        secretPrefix: { type: string, example: whsec_a1b2c3 }
        failingSince: { type: [string, "null"], format: date-time }
        lastSuccessAt: { type: [string, "null"], format: date-time }
        lastFailureAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
    CreatedWebhookEndpoint:
      allOf:
        - $ref: "#/components/schemas/WebhookEndpoint"
        - type: object
          required: [secret]
          properties:
            secret: { type: string, description: Shown once. Verify signatures with it. }
    WebhookDelivery:
      type: object
      required: [id, endpointId, eventId, eventType, status, attempts, nextAttemptAt, lastAttemptAt, responseStatus, responseBody, error, durationMs, replayOfId, createdAt]
      properties:
        id: { type: string, example: whd_01j9x7m3p5q7r9s1t3u5v7w9x1 }
        endpointId: { type: string }
        eventId: { type: string }
        eventType: { type: string }
        status: { type: string, enum: [pending, succeeded, failed] }
        attempts: { type: integer }
        nextAttemptAt: { type: [string, "null"], format: date-time }
        lastAttemptAt: { type: [string, "null"], format: date-time }
        responseStatus: { type: [integer, "null"] }
        responseBody: { type: [string, "null"], description: First 1,000 characters of the last response body. }
        error: { type: [string, "null"], description: Transport error of the last attempt. }
        durationMs: { type: [integer, "null"] }
        replayOfId: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
    SendSmsRequest:
      type: object
      required: [to, body, class]
      properties:
        to: { type: string, description: E.164., example: "+27821234567" }
        body: { type: string, maxLength: 1600 }
        senderId: { type: string, description: A registered alphanumeric sender id. }
        class: { $ref: "#/components/schemas/MessageClass" }
        scheduledAt: { type: string, format: date-time, description: Up to 30 days ahead. }
        tags:
          type: array
          maxItems: 10
          items: { type: string, maxLength: 64 }
    SendSmsResponse:
      type: object
      required: [id, status, mode, segments, credits]
      properties:
        id: { type: string, example: sm_01j9x7k2r4m8q3v5n6b7c8d9e0 }
        status: { type: string, enum: [queued, scheduled] }
        mode: { type: string, enum: [live, test] }
        segments: { type: integer, example: 1 }
        credits: { type: integer, example: 1 }
    SmsMessage:
      type: object
      required: [id, channel, class, status, to, from, segments, credits, tags, error, scheduledAt, createdAt, sentAt, deliveredAt, events]
      properties:
        id: { type: string }
        channel: { type: string, enum: [sms] }
        class: { $ref: "#/components/schemas/MessageClass" }
        status: { $ref: "#/components/schemas/MessageStatus" }
        to: { type: string }
        from: { type: string }
        segments: { type: integer }
        credits: { type: integer }
        tags: { type: array, items: { type: string } }
        error: { $ref: "#/components/schemas/MessageError" }
        scheduledAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
        sentAt: { type: [string, "null"], format: date-time }
        deliveredAt: { type: [string, "null"], format: date-time }
        events:
          type: array
          items: { $ref: "#/components/schemas/MessageEvent" }
    SmsCoverageCountry:
      type: object
      required: [code, name, dialCode, route, available, enabled, pricing, priceRand, provisional]
      properties:
        code: { type: string, example: ZA, description: ISO 3166-1 alpha-2 }
        name: { type: string, example: South Africa }
        dialCode: { type: string, example: "+27" }
        route:
          type: string
          enum: [southern-africa, pan-africa]
          description: "Delivery route serving the country: southern-africa (South Africa and its neighbours) or pan-africa (the rest of the continent)."
        available: { type: boolean, description: The route is live }
        enabled: { type: boolean, description: Your organisation has enabled this country on Settings }
        pricing:
          type: string
          enum: [plan, wallet]
          description: "plan: included allowance, then your plan's rate (South Africa). wallet: priceRand per segment from the wallet."
        priceRand: { type: [number, "null"], example: 1.2 }
        provisional: { type: boolean, description: Price set before the route's rate card; reviewed monthly }
        notes: { type: string }
    SmsCoverage:
      type: object
      required: [countries, enabled, available, settingsUrl]
      properties:
        countries:
          type: array
          items: { $ref: "#/components/schemas/SmsCoverageCountry" }
        enabled: { type: array, items: { type: string }, example: [ZA] }
        available: { type: array, items: { type: string }, example: [ZA, BW, SZ, LS, NA] }
        settingsUrl: { type: string, format: uri }
    Usage:
      type: object
      required: [plan, period, emails, sms, wallet, overage, domains]
      properties:
        plan:
          type: object
          properties:
            id: { type: string, enum: [free, starter, growth, scale, payg] }
            name: { type: string }
            priceRand: { type: number }
        period:
          type: object
          properties:
            start: { type: string, format: date-time }
            end: { type: string, format: date-time, description: When the allowance resets. }
        emails:
          type: object
          properties:
            used: { type: integer }
            included: { type: [integer, "null"] }
            remaining: { type: [integer, "null"] }
            daily:
              type: [object, "null"]
              description: Live emails counted today against the plan's daily cap; null when the plan has none.
              properties:
                used: { type: integer }
                cap: { type: integer }
        sms:
          type: object
          properties:
            used: { type: integer }
            included: { type: [integer, "null"] }
            remaining: { type: [integer, "null"] }
            credits: { type: integer, description: Prepaid SMS credits, used once the allowance is gone. }
        wallet:
          type: object
          description: The rand wallet. Pays sends beyond the allowance, and every send on pay as you go.
          properties:
            balanceRand: { type: number }
        overage:
          type: [object, "null"]
          description: Postpaid overage accepted this period and the spend cap it stops at; null on plans without overage.
          properties:
            emails: { type: integer }
            sms: { type: integer }
            amountRand: { type: number }
            spendCapRand: { type: number }
        domains:
          type: object
          properties:
            used: { type: integer }
            included: { type: [integer, "null"] }
        warmup:
          type: [object, "null"]
          description: Set while the organisation is warming up on the shared pool; null once complete.
          properties:
            day: { type: [integer, "null"], description: Day of warm-up, starting at 1. }
            cap: { type: [integer, "null"], description: Today's cap on live email. }
            sentToday: { type: integer }
            resetsAt: { type: string, format: date-time, description: Start of the next UTC day, when the cap advances. }
    DomainAdvice:
      type: object
      required: [code, level, title, detail]
      properties:
        code: { type: string, example: dmarc_missing }
        level: { type: string, enum: [warn, info] }
        title: { type: string }
        detail: { type: string }
    Deleted:
      type: object
      required: [id, deleted]
      properties:
        id: { type: string }
        deleted: { type: boolean, enum: [true] }
    ErrorBody:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Stable error code; see the errors reference.
          example: validation_error
        message: { type: string }
        docs: { type: string, format: uri }
        fields:
          type: object
          description: Present on validation errors. Dotted path to messages.
          additionalProperties:
            type: array
            items: { type: string }
        limit:
          type: integer
          description: On `plan_limit`, the ceiling that was reached.
        upgradeUrl:
          type: string
          format: uri
          description: Where to change plan, when a bigger plan would have allowed the request.
        topUpUrl:
          type: string
          format: uri
          description: Where to add wallet funds, when a balance would have allowed the request.
        settingsUrl:
          type: string
          format: uri
          description: Where to change an organisation setting that blocked the request (SMS destinations).
        domainsUrl:
          type: string
          format: uri
          description: Where to add and verify a sending domain, on `sandbox_recipient_not_allowed` and `sandbox_limit_reached`.
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error: { $ref: "#/components/schemas/ErrorBody" }
