openapi: 3.0.3
info:
  title: BytSend API
  version: 1.0.0
  description: |
    BytSend is a transactional email API (Resend-style). Send emails, manage
    sending domains and DNS verification, templates, contacts, audiences,
    suppressions, webhooks and analytics over a simple REST API.

    ## Base URL

    The hosted service is `https://bytsend.com` — that is the base URL unless
    you installed BytSend on your own server, in which case it is your own
    deployment's domain. It is a constant in the integrating app's code, never
    a form field: do not build an "instance URL" or "host" input.

    Integrating requires exactly two values: an API key (`by_…`) and a sender
    address on a domain registered to the account.

    ## Sending rules worth knowing before you build

    - `to` takes **one recipient** — a string or `{ email, name }`. An array is
      not rejected but misbehaves: the message goes to every address, the
      suppression list is skipped, and `to` is lost from the response and the
      stored record. For many recipients use `POST /api/emails/batch`, or
      `cc`/`bcc`.
    - The domain after the `@` in `from` must be registered on the account, or
      the send is rejected. This is the most common first-integration failure.
      Matching is exact — registering `example.com` does not authorise
      `@mail.example.com` — and DNS verification does not have to be finished:
      a still-`pending` domain is accepted here, though without SPF and DKIM
      the message tends to die at the relay.
    - Content is spam-scored before sending; 2 points rejects the message and
      3 rejections block the account. See `x-spam-filter` below.
    - A `template_id` that does not exist on the account is ignored silently —
      the email goes out with an empty subject and body and still returns 200.
    - Request bodies are capped at 10 MB, attachments included (base64 adds
      roughly 33% over the original file size). Over that the request fails
      before sending, as `500` with type `api_error` — not an `email_error`.
    - Of the six webhook event types, only `email.sent` is delivered today.

    ## Authentication

    Two credential types are accepted via the `Authorization: Bearer <token>` header:

    - **API keys** (`apiKeyAuth`) — keys prefixed with `by_`, created with
      `POST /api/api-keys` or in the dashboard. This is the recommended credential
      for server-to-server integrations. Endpoints tagged with both schemes accept
      either an API key or a JWT.
    - **JWT** (`bearerAuth`) — a 7-day token returned by `POST /api/auth/register`,
      `POST /api/auth/login`, `POST /api/auth/google` or
      `POST /api/auth/verify-email`. Required for account and billing endpoints.

    ## Errors

    Every error response uses the same envelope:

    ```json
    { "error": { "message": "Human-readable message", "type": "validation_error" } }
    ```

    Common `type` values: `validation_error`, `auth_error`, `api_key_error`,
    `not_found_error`, `conflict_error`, `plan_limit_error`, `rate_limit_error`,
    `email_error`, `api_error`.

    `error` is an object, not a string — render `error.message`.

    `POST /api/emails` reports every send failure as `400` with type
    `email_error`; the `message` is what distinguishes them:

    | Message starts with | Cause |
    |---|---|
    | `Domain X is not verified for your account` | The `from` domain is not registered on this account — despite the wording, the check is only that the domain exists there |
    | `Email X is suppressed` | Recipient is on the suppression list |
    | `Email limit reached for ... plan` | The account's email quota is spent |
    | `Spam detected: ...` | The spam filter rejected the content |
    | `Account blocked` | The account is blocked |
    | `Erro ao enviar via SMTP (...)` | The SMTP relay refused the message |

    ## Rate limits

    All `/api/` routes share a rate limit of **1000 requests per 15 minutes**
    per IP (`RateLimit-*` standard headers are returned). Exceeding it returns
    `429` with type `rate_limit_error`.
  contact:
    name: BytSend
    url: https://bytsend.com/docs
  license:
    name: MIT
servers:
  - url: https://bytsend.com
    description: Hosted BytSend service — use this unless you self-host
  - url: /
    description: The instance serving this spec (self-hosted deployments)
x-spam-filter:
  scored_fields: [subject, html, text]
  reject_at_score: 2
  keyword_points: 1
  keyword_count: 33
  keyword_matching: case-insensitive substring over subject + html + text
  keyword_note: >-
    The keyword list includes terms that appear in legitimate mail — among them
    "unsubscribe", "urgent", "limited time" and "order now". Two of them in one
    message are enough to reject it. "click here" is listed twice in the
    backend, so a single occurrence already scores 2 and rejects on its own.
  many_links_points: 2
  many_links_threshold: more than 5 http:// or https:// links in the body
  rejections_before_account_block: 3
  failure: '400 with type email_error and message "Spam detected: <terms>"'
x-max-request-body: 10mb
x-rate-limit:
  window: 15 minutes
  max_requests: 1000
  scope: per IP address, shared across all /api/ routes
  headers: RateLimit-Policy, RateLimit (standardHeaders)
tags:
  - name: Health
    description: Service liveness and readiness
  - name: Auth
    description: Account registration, login and session (JWT)
  - name: Emails
    description: Send and retrieve transactional emails
  - name: Domains
    description: Sending domains and DNS verification
  - name: API Keys
    description: Manage `by_` API keys
  - name: Contacts
    description: Contact list management
  - name: Templates
    description: Reusable email templates with {{variable}} placeholders
  - name: Audiences
    description: Audience segmentation for contacts
  - name: Webhooks
    description: Event endpoints and delivery logs
  - name: Suppressions
    description: Suppression list (bounces, complaints, manual blocks)
  - name: Analytics
    description: Sending metrics overview
  - name: Billing
    description: Asaas-backed subscription endpoints — PIX or credit card (JWT only)
paths:
  /api/health:
    get:
      tags: [Health]
      summary: Service health check
      operationId: getHealth
      security: []
      responses:
        '200':
          description: Service is ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthStatus'
              example:
                status: ok
                database:
                  storage: file
                  ready: true
                timestamp: '2026-01-15T10:30:00.000Z'
        '503':
          description: Service is degraded (database not ready)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthStatus'
  /api/auth/config:
    get:
      tags: [Auth]
      summary: Get public authentication configuration
      operationId: getAuthConfig
      security: []
      responses:
        '200':
          description: Which login/registration methods are enabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  google_client_id:
                    type: string
                    nullable: true
                  email_verification_enabled:
                    type: boolean
                  email_login_enabled:
                    type: boolean
                  email_registration_enabled:
                    type: boolean
                  google_login_enabled:
                    type: boolean
                  google_registration_enabled:
                    type: boolean
  /api/auth/register:
    post:
      tags: [Auth]
      summary: Create an account
      operationId: register
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 8
                name:
                  type: string
                locale:
                  type: string
                  enum: [en, pt, es]
                  default: en
            example:
              email: jane@example.com
              password: sup3r-secret
              name: Jane Doe
      responses:
        '201':
          description: |
            Account created. When email verification is required, the response has
            `requires_verification: true` and a `verification_token` JWT to use with
            `/api/auth/verify-email`; otherwise it includes a session `token` and a
            default API key named `Production` has been created for the account.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/User'
                  - type: object
                    properties:
                      token:
                        type: string
                        description: JWT session token (absent when verification is required)
                      requires_verification:
                        type: boolean
                      verification_token:
                        type: string
                        description: JWT to authorize verify-email / resend-verification
        '400':
          $ref: '#/components/responses/Error'
        '403':
          description: Email registration is disabled (type `registration_disabled`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: Email already registered (type `conflict_error`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: Verification email could not be sent (type `email_configuration_error`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/auth/login:
    post:
      tags: [Auth]
      summary: Log in with email and password
      operationId: login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
            example:
              email: jane@example.com
              password: sup3r-secret
      responses:
        '200':
          description: Authenticated; `token` is a 7-day JWT
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/User'
                  - type: object
                    properties:
                      token:
                        type: string
        '401':
          description: Invalid credentials (type `auth_error`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: |
            Sign-in blocked: `login_disabled`, `account_blocked`, or
            `email_not_verified` (the latter includes a `verification_token`
            alongside the error object).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/auth/google:
    post:
      tags: [Auth]
      summary: Log in or register with a Google ID token
      operationId: googleAuth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [credential]
              properties:
                credential:
                  type: string
                  description: Google ID token from Google Sign-In
                locale:
                  type: string
                  enum: [en, pt, es]
      responses:
        '200':
          description: Authenticated (account created on first use)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/User'
                  - type: object
                    properties:
                      token:
                        type: string
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '502':
          description: Google sign-in could not be completed (type `oauth_error`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: Google sign-in is not configured (type `oauth_configuration_error`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/auth/verify-email:
    post:
      tags: [Auth]
      summary: Verify email address with the 6-digit code
      operationId: verifyEmail
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  description: 6-digit code sent by email (15 min expiry, max 5 attempts)
            example:
              code: '482913'
      responses:
        '200':
          description: Email verified; returns the user with a session `token`
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/User'
                  - type: object
                    properties:
                      token:
                        type: string
        '400':
          description: Invalid or expired code (type `verification_error`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          $ref: '#/components/responses/Error'
        '429':
          description: Too many attempts (type `verification_error`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/auth/resend-verification:
    post:
      tags: [Auth]
      summary: Resend the verification code
      operationId: resendVerification
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Code sent (or already verified)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
              example:
                success: true
        '401':
          $ref: '#/components/responses/Error'
        '503':
          description: Verification email could not be sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/auth/me:
    get:
      tags: [Auth]
      summary: Get the current account (JWT only)
      operationId: getMe
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Current user with usage counters and plan limits
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/User'
                  - type: object
                    properties:
                      domain_count:
                        type: integer
                      email_sent_count:
                        type: integer
                      domain_limit:
                        type: integer
                      email_limit:
                        type: integer
        '401':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
    patch:
      tags: [Auth]
      summary: Update profile name or locale (JWT only)
      operationId: updateMe
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                locale:
                  type: string
                  enum: [en, pt, es]
            example:
              name: Jane D.
              locale: en
      responses:
        '200':
          description: Updated user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '401':
          $ref: '#/components/responses/Error'
  /api/auth/plans:
    get:
      tags: [Auth]
      summary: List active plans
      operationId: listPlans
      security: []
      responses:
        '200':
          description: Active plan catalog
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Plan'
  /api/emails:
    get:
      tags: [Emails]
      summary: List sent emails
      operationId: listEmails
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
          description: Page size
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
          description: Number of records to skip
      responses:
        '200':
          description: Paginated list, newest first
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Email'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
              example:
                data:
                  - id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                    from_email: hello@mail.example.com
                    from_name: Example App
                    to_email: user@example.org
                    subject: Welcome aboard
                    status: sent
                    last_event: sent
                    created_at: '2026-01-15T10:30:00.000Z'
                pagination:
                  limit: 20
                  offset: 0
                  total: 137
        '401':
          $ref: '#/components/responses/Error'
    post:
      tags: [Emails]
      summary: Send a single email
      operationId: sendEmail
      description: |
        Sends one transactional email. The `from` domain should be a domain you
        added and verified under `/api/domains`. Either supply `html`/`text` and
        `subject` directly, or pass `template_id` (plus optional
        `template_variables`) to render a stored template — `{{variable}}`
        placeholders in the template are replaced. Recipients on your suppression
        list are rejected, content is spam-scored, and the send counts against
        your plan's email limit. Returns `email_error` (HTTP 400) on failure.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendEmailRequest'
            examples:
              simple:
                summary: Minimal send
                value:
                  from: hello@mail.example.com
                  to: user@example.org
                  subject: Welcome aboard
                  html: <strong>Thanks for joining!</strong>
              named:
                summary: Named addresses and extras
                value:
                  from:
                    email: hello@mail.example.com
                    name: Example App
                  to:
                    email: user@example.org
                    name: Jane
                  subject: Your receipt
                  html: <p>Receipt attached.</p>
                  text: Receipt attached.
                  reply_to: support@example.com
                  cc: billing@example.org
                  headers:
                    X-Entity-Ref-ID: inv_1024
              templated:
                summary: Using a template
                value:
                  from: hello@mail.example.com
                  to: user@example.org
                  template_id: 3
                  template_variables:
                    name: Jane
      responses:
        '200':
          description: Email accepted and handed to the configured mail provider
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  from:
                    type: string
                  to:
                    type: string
                  created_at:
                    type: string
                    format: date-time
              example:
                id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                from: hello@mail.example.com
                to: user@example.org
                created_at: '2026-01-15T10:30:00.000Z'
        '400':
          description: |
            Send failed. Always type `email_error` — read `error.message` to
            tell the cases apart: unverified `from` domain (the most common),
            suppressed recipient, plan email limit reached, spam detected,
            blocked account, or SMTP delivery failure. See the message table in
            the API description.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          $ref: '#/components/responses/Error'
  /api/emails/batch:
    post:
      tags: [Emails]
      summary: Send a batch of emails
      operationId: sendBatch
      description: |
        Sends multiple emails sequentially. Each entry is processed independently:
        the response array contains one result per input, either
        `{ id, status: "success" }` or `{ status: "error", error }`, in order.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [emails]
              properties:
                emails:
                  type: array
                  items:
                    $ref: '#/components/schemas/SendEmailRequest'
            example:
              emails:
                - from: hello@mail.example.com
                  to: jane@example.org
                  subject: Hi Jane
                  html: <p>Hello Jane!</p>
                - from: hello@mail.example.com
                  to: john@example.org
                  subject: Hi John
                  html: <p>Hello John!</p>
      responses:
        '200':
          description: Per-email results, in request order
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/BatchResult'
              example:
                data:
                  - id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                    status: success
                  - status: error
                    error: 'Email bounced@example.org is suppressed (bounced)'
        '400':
          description: '`emails` is not an array (type `validation_error`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          $ref: '#/components/responses/Error'
  /api/emails/{id}:
    get:
      tags: [Emails]
      summary: Retrieve an email by ID
      operationId: getEmail
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Full stored email record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Email'
        '401':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
  /api/domains:
    get:
      tags: [Domains]
      summary: List sending domains
      operationId: listDomains
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      responses:
        '200':
          description: Domains with DNS records and status
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Domain'
        '401':
          $ref: '#/components/responses/Error'
    post:
      tags: [Domains]
      summary: Add a sending domain
      operationId: createDomain
      description: |
        Registers a root domain and generates the DNS records (ownership TXT,
        SPF, MX, DMARC, and DKIM once the platform finishes generating its
        keypair) that must be published before the domain becomes `active`.
        Subject to the plan's domain limit.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Root domain (scheme and trailing slash are stripped)
            example:
              name: mail.example.com
      responses:
        '201':
          description: Domain created with `pending` DNS records
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Domain'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          description: Plan domain limit reached (type `plan_limit_error`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          $ref: '#/components/responses/Error'
        '409':
          description: Domain already exists (type `conflict_error`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/domains/{id}:
    get:
      tags: [Domains]
      summary: Retrieve a domain
      operationId: getDomain
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Domain with DNS records
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Domain'
        '401':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
    delete:
      tags: [Domains]
      summary: Delete a domain
      operationId: deleteDomain
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Domain deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  id:
                    type: integer
              example:
                success: true
                id: 12
        '401':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
  /api/domains/{id}/verify:
    get:
      tags: [Domains]
      summary: Check DNS records and refresh domain status
      operationId: verifyDomain
      description: |
        Re-checks the domain's DNS records. Each record is marked `verified` or
        `pending`; when all records verify, the domain status becomes `active`.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Domain with refreshed record statuses
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Domain'
        '401':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
        '422':
          description: DNS verification could not run (includes `verification_error`)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Domain'
                  - type: object
                    properties:
                      verification_error:
                        type: string
  /api/api-keys:
    get:
      tags: [API Keys]
      summary: List API keys
      operationId: listApiKeys
      description: Key material is never returned after creation — only metadata.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      responses:
        '200':
          description: API key metadata for the account
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiKey'
              example:
                data:
                  - id: 1
                    name: Production
                    prefix: by_a1b2c3d4
                    created_at: '2026-01-10T09:00:00.000Z'
                    last_used_at: '2026-01-15T10:29:00.000Z'
                    active: 1
        '401':
          $ref: '#/components/responses/Error'
    post:
      tags: [API Keys]
      summary: Create an API key
      operationId: createApiKey
      description: |
        Creates a new `by_` API key. The full key is returned **once** in the
        `token` field — store it immediately.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
            example:
              name: Staging
      responses:
        '201':
          description: Key created; `token` shown only now
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  token:
                    type: string
                    description: Full API key (`by_...`), returned only at creation
                  prefix:
                    type: string
                  created_at:
                    type: string
                    format: date-time
              example:
                id: 2
                name: Staging
                token: by_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c
                prefix: by_9f8e7d6c
                created_at: '2026-01-15T10:30:00.000Z'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
  /api/api-keys/{id}:
    delete:
      tags: [API Keys]
      summary: Delete an API key
      operationId: deleteApiKey
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Key deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '401':
          $ref: '#/components/responses/Error'
  /api/contacts:
    get:
      tags: [Contacts]
      summary: List contacts
      operationId: listContacts
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      responses:
        '200':
          description: All contacts for the account
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Error'
    post:
      tags: [Contacts]
      summary: Create a contact
      operationId: createContact
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                first_name:
                  type: string
                last_name:
                  type: string
            example:
              email: jane@example.org
              first_name: Jane
              last_name: Doe
      responses:
        '201':
          description: Contact created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
  /api/contacts/{id}:
    delete:
      tags: [Contacts]
      summary: Delete a contact
      operationId: deleteContact
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Contact deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '401':
          $ref: '#/components/responses/Error'
  /api/templates:
    get:
      tags: [Templates]
      summary: List templates
      operationId: listTemplates
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      responses:
        '200':
          description: All templates for the account
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Template'
        '401':
          $ref: '#/components/responses/Error'
    post:
      tags: [Templates]
      summary: Create a template
      operationId: createTemplate
      description: |
        Templates support `{{variable}}` placeholders in `subject`, `html` and
        `text`. Pass `template_id` and `template_variables` to
        `POST /api/emails` to render one at send time.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, subject]
              properties:
                name:
                  type: string
                subject:
                  type: string
                html:
                  type: string
                text:
                  type: string
            example:
              name: welcome
              subject: Welcome, {{name}}!
              html: <h1>Hi {{name}}</h1><p>Thanks for joining.</p>
              text: Hi {{name}}, thanks for joining.
      responses:
        '201':
          description: Template created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Template'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
  /api/templates/{id}:
    delete:
      tags: [Templates]
      summary: Delete a template
      operationId: deleteTemplate
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Template deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '401':
          $ref: '#/components/responses/Error'
  /api/audiences:
    get:
      tags: [Audiences]
      summary: List audiences
      operationId: listAudiences
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      responses:
        '200':
          description: All audiences for the account
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Audience'
        '401':
          $ref: '#/components/responses/Error'
    post:
      tags: [Audiences]
      summary: Create an audience
      operationId: createAudience
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
            example:
              name: Newsletter subscribers
      responses:
        '201':
          description: Audience created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Audience'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
  /api/audiences/{id}:
    delete:
      tags: [Audiences]
      summary: Delete an audience
      operationId: deleteAudience
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Audience deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '401':
          $ref: '#/components/responses/Error'
  /api/audiences/{id}/contacts:
    get:
      tags: [Audiences]
      summary: List contacts in an audience
      operationId: listAudienceContacts
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Contacts assigned to this audience
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Error'
  /api/webhooks:
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      operationId: listWebhooks
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      responses:
        '200':
          description: Configured webhook endpoints
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEndpoint'
        '401':
          $ref: '#/components/responses/Error'
    post:
      tags: [Webhooks]
      summary: Create a webhook endpoint
      operationId: createWebhook
      description: |
        Registers an HTTPS endpoint that receives event POSTs. Every delivery
        carries `BytSend-Signature` (hex HMAC-SHA256 of
        `<timestamp>.<raw JSON body>` keyed with the instance `WEBHOOK_SECRET`)
        and `BytSend-Timestamp` headers so receivers can verify authenticity.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - email.sent
                      - email.delivered
                      - email.bounced
                      - email.complained
                      - email.opened
                      - email.clicked
                  default: [email.sent, email.delivered, email.bounced, email.complained, email.opened, email.clicked]
            example:
              url: https://api.example.org/hooks/bytsend
              events: [email.sent, email.bounced]
      responses:
        '201':
          description: Webhook endpoint created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
  /api/webhooks/{id}:
    delete:
      tags: [Webhooks]
      summary: Delete a webhook endpoint
      operationId: deleteWebhook
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Webhook endpoint deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '401':
          $ref: '#/components/responses/Error'
  /api/webhooks/{id}/deliveries:
    get:
      tags: [Webhooks]
      summary: List delivery attempts for a webhook
      operationId: listWebhookDeliveries
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Delivery attempts (pending, delivered or failed)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookDelivery'
        '401':
          $ref: '#/components/responses/Error'
  /api/suppressions:
    get:
      tags: [Suppressions]
      summary: List suppressed addresses
      operationId: listSuppressions
      description: Sends to suppressed addresses are rejected with an `email_error`.
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      responses:
        '200':
          description: Suppression list for the account
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Suppression'
        '401':
          $ref: '#/components/responses/Error'
    post:
      tags: [Suppressions]
      summary: Add an address to the suppression list
      operationId: createSuppression
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                reason:
                  type: string
                  default: manual
                  description: e.g. manual, bounced, complained
            example:
              email: bounced@example.org
              reason: bounced
      responses:
        '201':
          description: Suppression created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Suppression'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '409':
          description: Address already suppressed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/suppressions/{id}:
    delete:
      tags: [Suppressions]
      summary: Remove an address from the suppression list
      operationId: deleteSuppression
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/NumericId'
      responses:
        '200':
          description: Suppression removed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '401':
          $ref: '#/components/responses/Error'
  /api/analytics:
    get:
      tags: [Analytics]
      summary: Sending metrics overview
      operationId: getAnalytics
      security:
        - apiKeyAuth: []
        - bearerAuth: []
      responses:
        '200':
          description: Totals, per-domain counts and the last 7 days of volume
          content:
            application/json:
              schema:
                type: object
                properties:
                  overview:
                    type: object
                    properties:
                      total:
                        type: integer
                      sent:
                        type: integer
                      failed:
                        type: integer
                      today:
                        type: integer
                  domains:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        sent:
                          type: integer
                  last_7_days:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        count:
                          type: integer
              example:
                overview:
                  total: 137
                  sent: 131
                  failed: 6
                  today: 12
                domains:
                  - name: mail.example.com
                    sent: 137
                last_7_days:
                  - date: '2026-01-09'
                    count: 18
                  - date: '2026-01-10'
                    count: 21
        '401':
          $ref: '#/components/responses/Error'
  /api/billing/create-checkout-session:
    post:
      tags: [Billing]
      summary: Start a subscription charge for a plan via Asaas (JWT only)
      operationId: createCheckoutSession
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plan]
              properties:
                plan:
                  type: string
                  description: Plan ID (see GET /api/auth/plans)
            example:
              plan: starter
      responses:
        '200':
          description: Redirect the user to `url` to complete checkout
          content:
            application/json:
              schema:
                type: object
                properties:
                  sessionId:
                    type: string
                  url:
                    type: string
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '502':
          description: Payment provider error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: The payment provider is not configured (type `billing_configuration_error`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/billing/subscription:
    get:
      tags: [Billing]
      summary: Get current subscription status (JWT only)
      operationId: getSubscription
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Plan and active subscription for the account
          content:
            application/json:
              schema:
                type: object
                properties:
                  configured:
                    type: boolean
                    description: Whether the payment provider is configured on this instance
                  plan:
                    type: string
                  customer:
                    type: boolean
                  subscription:
                    type: object
                    nullable: true
        '401':
          $ref: '#/components/responses/Error'
components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: Authorization
      description: |
        BytSend API key (prefix `by_`), passed as a Bearer token:
        `Authorization: Bearer by_your_api_key`. Create keys with
        `POST /api/api-keys` or in the dashboard. Accepted on all resource
        endpoints (Emails, Domains, API Keys, Contacts, Templates, Audiences,
        Webhooks, Suppressions, Analytics).
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        7-day JWT returned by `/api/auth/register`, `/api/auth/login`,
        `/api/auth/google` or `/api/auth/verify-email`. Required for
        `/api/auth/*` session endpoints and `/api/billing/*`. Also accepted on
        resource endpoints wherever `apiKeyAuth` is listed.
  parameters:
    NumericId:
      name: id
      in: path
      required: true
      schema:
        type: integer
  responses:
    Error:
      description: Error response (see `ErrorEnvelope` schema)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  schemas:
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [message]
          properties:
            message:
              type: string
              description: Human-readable description
            type:
              type: string
              description: |
                Machine-readable category, e.g. `validation_error`,
                `auth_error`, `api_key_error`, `not_found_error`,
                `conflict_error`, `plan_limit_error`, `rate_limit_error`,
                `email_error`, `api_error`. A few endpoints omit `type`.
      example:
        error:
          message: Enter a valid root domain
          type: validation_error
    SuccessResponse:
      type: object
      properties:
        success:
          type: boolean
      example:
        success: true
    Pagination:
      type: object
      properties:
        limit:
          type: integer
        offset:
          type: integer
        total:
          type: integer
    HealthStatus:
      type: object
      properties:
        status:
          type: string
          enum: [ok, degraded]
        database:
          type: object
        timestamp:
          type: string
          format: date-time
    User:
      type: object
      properties:
        id:
          type: integer
        email:
          type: string
          format: email
        name:
          type: string
        plan:
          type: string
          example: free
        role:
          type: string
          enum: [user, admin]
        locale:
          type: string
          enum: [en, pt, es]
        verified:
          type: boolean
        status:
          type: string
          example: active
    Plan:
      type: object
      properties:
        id:
          type: string
          example: starter
        name:
          type: string
        description:
          type: string
        monthly_price:
          type: number
        currency:
          type: string
          example: USD
        email_limit:
          type: integer
        domain_limit:
          type: integer
        features:
          type: array
          items:
            type: string
        active:
          type: boolean
        highlighted:
          type: boolean
    Address:
      description: >-
        A plain email, an RFC address string ("Name <email>"), or an object
        with email and name. All three are parsed the same way — the domain
        checked against the account's registered domains is always the part
        after the @ in the email address, matched exactly.
      oneOf:
        - type: string
          format: email
        - type: string
          example: 'Jane Doe <jane@example.org>'
        - type: object
          required: [email]
          properties:
            email:
              type: string
              format: email
            name:
              type: string
    Attachment:
      type: object
      description: Passed through to the mail provider (nodemailer format)
      properties:
        filename:
          type: string
        content:
          type: string
          description: Base64-encoded content
        content_type:
          type: string
    SendEmailRequest:
      type: object
      required: [from, to]
      properties:
        from:
          allOf:
            - $ref: '#/components/schemas/Address'
          description: >-
            Sender. The domain after the `@` must be registered on this
            account (exact match), otherwise the send is rejected with
            `email_error`.
        to:
          allOf:
            - $ref: '#/components/schemas/Address'
          description: >-
            One recipient. An array is not rejected but misbehaves — it is
            delivered to every address, the suppression list is skipped, and
            `to` is lost from the response and the stored record. For several
            recipients use `POST /api/emails/batch`, or `cc`/`bcc`.
        subject:
          type: string
          description: Required unless `template_id` is used
        html:
          type: string
        text:
          type: string
        reply_to:
          type: string
          format: email
        cc:
          type: string
          format: email
        bcc:
          type: string
          format: email
        headers:
          type: object
          additionalProperties:
            type: string
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/Attachment'
        template_id:
          type: integer
          description: Render this stored template instead of html/text/subject
        template_variables:
          type: object
          additionalProperties: true
          description: Values for the template's {{variable}} placeholders
    Email:
      type: object
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: integer
        api_key_id:
          type: integer
          nullable: true
        from_email:
          type: string
        from_name:
          type: string
        to_email:
          type: string
        to_name:
          type: string
        subject:
          type: string
        html:
          type: string
        text:
          type: string
        reply_to:
          type: string
          nullable: true
        cc:
          type: string
          nullable: true
        bcc:
          type: string
          nullable: true
        headers:
          type: object
          nullable: true
        attachments:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/Attachment'
        domain_id:
          type: integer
          nullable: true
        status:
          type: string
          enum: [sent, failed]
        last_event:
          type: string
          example: sent
        provider:
          type: string
          nullable: true
        provider_message_id:
          type: string
          nullable: true
        error:
          type: string
          nullable: true
          description: Present when status is `failed`
        created_at:
          type: string
          format: date-time
    BatchResult:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Present on success
        status:
          type: string
          enum: [success, error]
        error:
          type: string
          description: Present on failure
    DnsRecord:
      type: object
      properties:
        type:
          type: string
          example: TXT
        name:
          type: string
          example: _bytsend-verify.mail.example.com
        value:
          type: string
          example: bytsend-verify=9f8e7d6c5b4a3928
        ttl:
          type: string
          example: Auto
        priority:
          type: integer
          nullable: true
          description: Set on the MX record only; null for TXT records.
        status:
          type: string
          enum: [pending, verified, not_configured]
          description: dkim starts as `not_configured` until the platform generates its keypair.
    Domain:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        name:
          type: string
          example: mail.example.com
        status:
          type: string
          enum: [pending, active]
        region:
          type: string
          example: sa-east-1
        records:
          type: object
          properties:
            ownership:
              $ref: '#/components/schemas/DnsRecord'
            spf:
              $ref: '#/components/schemas/DnsRecord'
            mx:
              $ref: '#/components/schemas/DnsRecord'
            dmarc:
              $ref: '#/components/schemas/DnsRecord'
            dkim:
              $ref: '#/components/schemas/DnsRecord'
          description: ownership is only present while the domain still has a pending ownership_token to prove.
        zone_id:
          type: string
          nullable: true
        cf_status:
          type: string
          nullable: true
        cf_error:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    ApiKey:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        prefix:
          type: string
          description: First characters of the key, e.g. `by_a1b2c3d4`
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
          nullable: true
        active:
          type: integer
          enum: [0, 1]
    Contact:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        email:
          type: string
          format: email
        first_name:
          type: string
        last_name:
          type: string
        audience_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
    Template:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        name:
          type: string
        subject:
          type: string
        html:
          type: string
        text:
          type: string
        created_at:
          type: string
          format: date-time
    Audience:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        name:
          type: string
        created_at:
          type: string
          format: date-time
    WebhookEndpoint:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            type: string
        active:
          type: integer
          enum: [0, 1]
        created_at:
          type: string
          format: date-time
    WebhookDelivery:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        webhook_id:
          type: integer
        event:
          type: string
          example: email.sent
        payload:
          type: string
          description: JSON-serialized event payload (on the initial attempt)
        signature:
          type: string
          description: '`whsec_`-prefixed HMAC sent in BytSend-Signature'
        status:
          type: string
          enum: [pending, delivered, failed]
        url:
          type: string
        response_status:
          type: integer
          nullable: true
        error:
          type: string
          nullable: true
        parent_id:
          type: integer
          nullable: true
          description: Links a delivery result row to its initial attempt row
        created_at:
          type: string
          format: date-time
    Suppression:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        email:
          type: string
          format: email
        reason:
          type: string
          example: manual
        created_at:
          type: string
          format: date-time
    WebhookEvent:
      description: |
        Payload POSTed to your webhook URL. Verified with the
        `BytSend-Signature` header: compute
        `HMAC_SHA256(WEBHOOK_SECRET, "<BytSend-Timestamp>.<raw body>")` and
        compare (constant-time) with the hex signature.
      type: object
      properties:
        type:
          type: string
          enum: [email.sent, email.delivered, email.bounced, email.complained, email.opened, email.clicked]
        created_at:
          type: string
          format: date-time
        data:
          type: object
          properties:
            object:
              type: string
              example: email
            email_id:
              type: string
              format: uuid
            to:
              type: string
            subject:
              type: string
            created_at:
              type: string
              format: date-time
      example:
        type: email.sent
        created_at: '2026-01-15T10:30:01.000Z'
        data:
          object: email
          email_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
          to: user@example.org
          subject: Welcome aboard
          created_at: '2026-01-15T10:30:00.000Z'
