openapi: 3.1.0
info:
  title: Precisent REST API
  version: "1.0.0"
  description: >
    The Precisent developer API (FEATURES §9). Every request authenticates a
    Sanctum bearer token (`Authorization: Bearer <token>`); the token is
    account-scoped and permission-scoped, so a key can only reach endpoints whose
    ability it carries and only ever sees its own account's data. Successful
    responses use a `{ "data", "meta" }` envelope; failures use
    `{ "error": { code, message, details } }`. List endpoints use keyset (cursor)
    pagination — pass `?per_page=` (max 1000) and follow `meta.next_cursor` with
    `?cursor=`. Unsafe writes accept an `Idempotency-Key` header; a replay returns
    the first result. Bulk/expensive calls return `202` with an id to poll.
servers:
  - url: /api/v1
    description: API v1

security:
  - bearerAuth: []

tags:
  - name: Meta
  - name: Contacts
  - name: Lists
  - name: Tags
  - name: Segments
  - name: Campaigns
  - name: Suppressions
  - name: Events
  - name: Messages
  - name: Exports
  - name: Operations
  - name: Webhooks

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A Sanctum personal-access token minted under Settings → API keys.

  parameters:
    PerPage:
      name: per_page
      in: query
      required: false
      schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
      description: Page size (keyset pagination).
    Cursor:
      name: cursor
      in: query
      required: false
      schema: { type: string }
      description: The `meta.next_cursor` from the previous page.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string }
      description: Replays the first result for a repeated unsafe write.
    Email:
      name: email
      in: path
      required: true
      schema: { type: string, format: email }

  schemas:
    Meta:
      type: object
      properties:
        next_cursor: { type: [string, "null"] }
        per_page: { type: integer }
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, example: validation_error }
            message: { type: string }
            details: { type: object, additionalProperties: true }
    Contact:
      type: object
      properties:
        email: { type: string, format: email }
        status: { type: string, example: subscribed }
        custom_fields: { type: object, additionalProperties: true }
        opens: { type: integer }
        clicks: { type: integer }
        created_at: { type: [string, "null"], format: date-time }
    List:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        from_name: { type: [string, "null"] }
        from_email: { type: [string, "null"] }
        visibility: { type: string, enum: [account, user] }
        created_at: { type: [string, "null"], format: date-time }
    Tag:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        visibility: { type: string, enum: [account, user] }
        created_at: { type: [string, "null"], format: date-time }
    Segment:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        definition: { type: object, additionalProperties: true }
        cached_count: { type: [integer, "null"] }
        counted_at: { type: [string, "null"], format: date-time }
        visibility: { type: string, enum: [account, user] }
    Campaign:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        subject: { type: string }
        from_name: { type: string }
        from_email: { type: string, format: email }
        reply_to: { type: [string, "null"], format: email }
        status: { type: string, example: draft }
        email_list_id: { type: integer }
        scheduled_at: { type: [string, "null"], format: date-time }
        sent_at: { type: [string, "null"], format: date-time }
    CampaignStat:
      type: object
      properties:
        campaign_id: { type: integer }
        sent: { type: integer }
        delivered: { type: integer }
        opens: { type: integer }
        clicks: { type: integer }
        bounces: { type: integer }
        complaints: { type: integer }
        unsubscribes: { type: integer }
        counted_at: { type: [string, "null"], format: date-time }
    Suppression:
      type: object
      properties:
        email: { type: string, format: email }
        reason: { type: string, enum: [hard_bounce, soft_bounce_threshold, complaint, manual, banned] }
        created_at: { type: [string, "null"], format: date-time }
    TransientSuppression:
      type: object
      properties:
        email: { type: string, format: email }
        reason: { type: [string, "null"] }
        expires_at: { type: string, format: date-time }
        created_at: { type: [string, "null"], format: date-time }
    Event:
      type: object
      properties:
        type: { type: string }
        url: { type: [string, "null"] }
        meta: { type: object, additionalProperties: true }
        occurred_at: { type: [string, "null"], format: date-time }
    Webhook:
      type: object
      properties:
        id: { type: integer }
        url: { type: string, format: uri }
        events: { type: array, items: { type: string } }
        active: { type: boolean }
        secret: { type: string, description: Returned only on creation. }
        created_at: { type: [string, "null"], format: date-time }
    Delivery:
      type: object
      properties:
        id: { type: integer }
        event_type: { type: string }
        event_id: { type: string }
        status: { type: string, enum: [pending, retrying, delivered, failed] }
        attempts: { type: integer }
        response_code: { type: [integer, "null"] }
        delivered_at: { type: [string, "null"], format: date-time }
        next_retry_at: { type: [string, "null"], format: date-time }
    Export:
      type: object
      properties:
        id: { type: integer }
        status: { type: string, example: pending }
        row_count: { type: [integer, "null"] }
        download_url: { type: [string, "null"] }
        created_at: { type: [string, "null"], format: date-time }
    Operation:
      type: object
      properties:
        id: { type: integer }
        type: { type: string, example: contacts.bulk }
        status: { type: string, enum: [queued, processing, completed, failed] }
        total: { type: [integer, "null"] }
        processed: { type: integer }
        result: { type: [object, "null"], additionalProperties: true }

  responses:
    Unauthorized:
      description: Missing or invalid token.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: The token lacks the required ability, or the tenant boundary was crossed.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: No such resource in this account.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    ValidationError:
      description: The request failed validation.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

paths:
  /me:
    get:
      tags: [Meta]
      summary: Identify the calling key
      responses:
        "200":
          description: The key's account and abilities.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      account: { type: object, properties: { id: { type: integer }, name: { type: string } } }
                      abilities: { type: array, items: { type: string } }
              example:
                data:
                  account: { id: 1, name: "Acme" }
                  abilities: ["contacts:read", "contacts:write", "campaigns:send"]
        "401": { $ref: '#/components/responses/Unauthorized' }

  /contacts:
    get:
      tags: [Contacts]
      summary: List contacts
      parameters:
        - { name: tag, in: query, required: false, schema: { type: string } }
        - { name: segment, in: query, required: false, schema: { type: integer } }
        - $ref: '#/components/parameters/PerPage'
        - $ref: '#/components/parameters/Cursor'
      responses:
        "200":
          description: A page of contacts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Contact' } }
                  meta: { $ref: '#/components/schemas/Meta' }
              example:
                data:
                  - { id: 101, email: "ada@example.com", status: "subscribed" }
                  - { id: 102, email: "grace@example.com", status: "subscribed" }
                meta: { next_cursor: "eyJpZCI6MTAyfQ", per_page: 25 }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Contacts]
      summary: Upsert a contact by email
      parameters: [ { $ref: '#/components/parameters/IdempotencyKey' } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                custom_fields: { type: object, additionalProperties: true }
            example: { email: "ada@example.com", custom_fields: { plan: "gold" } }
      responses:
        "200":
          description: The contact.
          content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Contact' } } } } }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
        "422": { $ref: '#/components/responses/ValidationError' }

  /contacts/bulk:
    post:
      tags: [Contacts]
      summary: Bulk-upsert contacts (async)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contacts]
              properties:
                contacts:
                  type: array
                  maxItems: 1000
                  items:
                    type: object
                    required: [email]
                    properties:
                      email: { type: string, format: email }
                      custom_fields: { type: object, additionalProperties: true }
      responses:
        "202":
          description: Accepted; poll the operation.
          content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { operation_id: { type: integer }, status: { type: string } } } } } } }
        "403": { $ref: '#/components/responses/Forbidden' }
        "422": { $ref: '#/components/responses/ValidationError' }

  /contacts/{email}:
    parameters: [ { $ref: '#/components/parameters/Email' } ]
    get:
      tags: [Contacts]
      summary: Fetch a contact
      responses:
        "200": { description: The contact., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Contact' } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Contacts]
      summary: Delete a contact
      responses:
        "200": { description: Deleted., content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { deleted: { type: boolean } } } } } } } }
        "403": { $ref: '#/components/responses/Forbidden' }
        "404": { $ref: '#/components/responses/NotFound' }

  /contacts/{email}/events:
    parameters: [ { $ref: '#/components/parameters/Email' }, { $ref: '#/components/parameters/PerPage' }, { $ref: '#/components/parameters/Cursor' } ]
    get:
      tags: [Contacts]
      summary: A contact's events
      responses:
        "200": { description: A page of events., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Event' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /contacts/{email}/tags:
    parameters: [ { $ref: '#/components/parameters/Email' } ]
    post:
      tags: [Tags]
      summary: Apply a tag to a contact
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [tag], properties: { tag: { type: integer } } } } }
      responses:
        "200": { description: Tagged., content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { tagged: { type: boolean } } } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /contacts/{email}/tags/{tag}:
    parameters: [ { $ref: '#/components/parameters/Email' }, { name: tag, in: path, required: true, schema: { type: integer } } ]
    delete:
      tags: [Tags]
      summary: Remove a tag from a contact
      responses:
        "200": { description: Untagged., content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { untagged: { type: boolean } } } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /lists:
    get:
      tags: [Lists]
      summary: List lists
      parameters: [ { $ref: '#/components/parameters/PerPage' }, { $ref: '#/components/parameters/Cursor' } ]
      responses:
        "200": { description: A page of lists., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/List' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
    post:
      tags: [Lists]
      summary: Create a list
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [name], properties: { name: { type: string }, from_name: { type: string }, from_email: { type: string, format: email } } }, example: { name: "Newsletter", from_name: "Acme", from_email: "news@acme.com" } } }
      responses:
        "201": { description: The created list., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/List' } } }, example: { data: { id: 12, name: "Newsletter" } } } } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /lists/{list}/contacts:
    parameters: [ { name: list, in: path, required: true, schema: { type: integer } }, { $ref: '#/components/parameters/PerPage' }, { $ref: '#/components/parameters/Cursor' } ]
    get:
      tags: [Lists]
      summary: A list's contacts
      responses:
        "200": { description: A page of contacts., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Contact' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /lists/{list}/subscribe:
    parameters: [ { name: list, in: path, required: true, schema: { type: integer } } ]
    post:
      tags: [Lists]
      summary: Subscribe a contact to a list
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [email], properties: { email: { type: string, format: email }, custom_fields: { type: object, additionalProperties: true } } }, example: { email: "ada@example.com", custom_fields: { plan: "gold" } } } }
      responses:
        "200": { description: The subscribed contact., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Contact' } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /lists/{list}/unsubscribe:
    parameters: [ { name: list, in: path, required: true, schema: { type: integer } } ]
    post:
      tags: [Lists]
      summary: Unsubscribe a contact from a list
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [email], properties: { email: { type: string, format: email } } } } }
      responses:
        "200": { description: Unsubscribed., content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { unsubscribed: { type: boolean } } } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /tags:
    get:
      tags: [Tags]
      summary: List tags
      parameters: [ { $ref: '#/components/parameters/PerPage' }, { $ref: '#/components/parameters/Cursor' } ]
      responses:
        "200": { description: A page of tags., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Tag' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
    post:
      tags: [Tags]
      summary: Create a tag
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [name], properties: { name: { type: string } } }, example: { name: "VIP" } } }
      responses:
        "201": { description: The created tag., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Tag' } } }, example: { data: { id: 5, name: "VIP" } } } } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /segments:
    get:
      tags: [Segments]
      summary: List segments
      parameters: [ { $ref: '#/components/parameters/PerPage' }, { $ref: '#/components/parameters/Cursor' } ]
      responses:
        "200": { description: A page of segments., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Segment' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
    post:
      tags: [Segments]
      summary: Create a segment
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [name, definition], properties: { name: { type: string }, definition: { type: object, additionalProperties: true } } }, example: { name: "Engaged VIPs", definition: { all: [ { field: "tag", op: "eq", value: "VIP" }, { field: "opens", op: "gte", value: 3 } ] } } } }
      responses:
        "201": { description: The created segment., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Segment' } } } } } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /segments/{segment}:
    parameters: [ { name: segment, in: path, required: true, schema: { type: integer } } ]
    patch:
      tags: [Segments]
      summary: Update a segment
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, properties: { name: { type: string }, definition: { type: object, additionalProperties: true } } } } }
      responses:
        "200": { description: The updated segment., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Segment' } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /segments/{segment}/count:
    parameters: [ { name: segment, in: path, required: true, schema: { type: integer } } ]
    get:
      tags: [Segments]
      summary: Count a segment's members
      responses:
        "200": { description: The count., content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { count: { type: integer } } } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /segments/{segment}/contacts:
    parameters: [ { name: segment, in: path, required: true, schema: { type: integer } }, { $ref: '#/components/parameters/PerPage' }, { $ref: '#/components/parameters/Cursor' } ]
    get:
      tags: [Segments]
      summary: A segment's members
      responses:
        "200": { description: A page of contacts., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Contact' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /campaigns:
    get:
      tags: [Campaigns]
      summary: List campaigns
      parameters: [ { $ref: '#/components/parameters/PerPage' }, { $ref: '#/components/parameters/Cursor' } ]
      responses:
        "200": { description: A page of campaigns., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Campaign' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
    post:
      tags: [Campaigns]
      summary: Create a campaign
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email_list_id, name, subject, from_name, from_email, body]
              properties:
                email_list_id: { type: integer }
                name: { type: string }
                subject: { type: string }
                from_name: { type: string }
                from_email: { type: string, format: email }
                reply_to: { type: string, format: email }
                body: { type: string }
            example:
              email_list_id: 12
              name: "March Newsletter"
              subject: "What's new in March"
              from_name: "Acme"
              from_email: "news@acme.com"
              reply_to: "hello@acme.com"
              body: "<h1>Hi {{ first_name }}</h1>"
      responses:
        "201":
          description: The created campaign.
          content:
            application/json:
              schema: { type: object, properties: { data: { $ref: '#/components/schemas/Campaign' } } }
              example: { data: { id: 34, name: "March Newsletter", status: "draft", subject: "What's new in March" } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /campaigns/{campaign}/stats:
    parameters: [ { name: campaign, in: path, required: true, schema: { type: integer } } ]
    get:
      tags: [Campaigns]
      summary: Campaign rollup stats
      responses:
        "200": { description: The stats., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/CampaignStat' } } }, example: { data: { sent: 10000, delivered: 9850, opens: 4200, machine_opens: 900, clicks: 1300, bounces: 150, complaints: 8, unsubscribes: 21 } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /campaigns/{campaign}/schedule:
    parameters: [ { name: campaign, in: path, required: true, schema: { type: integer } } ]
    post:
      tags: [Campaigns]
      summary: Schedule (or send) a campaign
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [scheduled_at], properties: { scheduled_at: { type: string, format: date-time } } }, example: { scheduled_at: "2026-09-01T14:00:00Z" } } }
      responses:
        "200": { description: The campaign., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Campaign' } } }, example: { data: { id: 34, status: "scheduled" } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /campaigns/{campaign}/cancel:
    parameters: [ { name: campaign, in: path, required: true, schema: { type: integer } } ]
    post:
      tags: [Campaigns]
      summary: Cancel a campaign
      responses:
        "200": { description: The campaign., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Campaign' } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /suppressions:
    get:
      tags: [Suppressions]
      summary: List suppressions
      parameters: [ { $ref: '#/components/parameters/PerPage' }, { $ref: '#/components/parameters/Cursor' } ]
      responses:
        "200": { description: A page of suppressions., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Suppression' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
    post:
      tags: [Suppressions]
      summary: Bannedlist an address
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [email], properties: { email: { type: string, format: email }, reason: { type: string, enum: [hard_bounce, soft_bounce_threshold, complaint, manual, banned] } } }, example: { email: "spam@example.com", reason: "manual" } } }
      responses:
        "201": { description: The suppression., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Suppression' } } }, example: { data: { email: "spam@example.com", reason: "manual" } } } } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /suppressions/{email}:
    parameters: [ { $ref: '#/components/parameters/Email' } ]
    delete:
      tags: [Suppressions]
      summary: Remove an address from the bannedlist
      responses:
        "200": { description: Removed flag., content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { removed: { type: boolean } } } } } } } }

  /bounces:
    get:
      tags: [Suppressions]
      summary: List bounce suppressions
      parameters: [ { $ref: '#/components/parameters/PerPage' }, { $ref: '#/components/parameters/Cursor' } ]
      responses:
        "200": { description: A page of bounce suppressions., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Suppression' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }

  /suppressions/transient:
    get:
      tags: [Suppressions]
      summary: List active transient (TTL) suppressions
      parameters: [ { $ref: '#/components/parameters/PerPage' }, { $ref: '#/components/parameters/Cursor' } ]
      responses:
        "200": { description: A page of active transient suppressions., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/TransientSuppression' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
    post:
      tags: [Suppressions]
      summary: Place or extend a transient (TTL) suppression
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [email, hours], properties: { email: { type: string, format: email }, hours: { type: integer, minimum: 1, maximum: 8760 }, reason: { type: string } } }, example: { email: "cool@example.com", hours: 24, reason: "cool_off" } } }
      responses:
        "201": { description: The transient suppression., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/TransientSuppression' } } }, example: { data: { email: "cool@example.com", reason: "cool_off", expires_at: "2026-08-25T12:00:00+00:00" } } } } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /events/track:
    post:
      tags: [Events]
      summary: Track a custom event
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, event]
              properties:
                email: { type: string, format: email }
                event: { type: string, example: purchase }
                properties: { type: object, additionalProperties: true }
            example:
              email: "ada@example.com"
              event: "purchase"
              properties: { order_id: "A-1001", total: 49.99, currency: "USD" }
      responses:
        "202":
          description: Accepted.
          content:
            application/json:
              schema: { type: object, properties: { data: { type: object, properties: { status: { type: string } } } } }
              example: { data: { status: "queued" } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /events/visit:
    post:
      tags: [Events]
      summary: Track a first-party website visit
      description: >
        Report that a known contact viewed a URL on your website. The visit is
        recorded as a `visit` event and can start a website-visit journey
        (optionally filtered by a URL substring on the trigger).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, url]
              properties:
                email: { type: string, format: email }
                url: { type: string, example: "https://shop.example.com/pricing" }
            example:
              email: "ada@example.com"
              url: "https://shop.example.com/pricing"
      responses:
        "202":
          description: Accepted.
          content:
            application/json:
              schema: { type: object, properties: { data: { type: object, properties: { status: { type: string } } } } }
              example: { data: { status: "accepted" } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /messages:
    post:
      tags: [Messages]
      summary: Send a transactional message
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, body, subject, from_email, from_name]
              properties:
                email: { type: string, format: email }
                body: { type: string }
                subject: { type: string }
                from_email: { type: string, format: email }
                from_name: { type: string }
                reply_to: { type: string, format: email }
            example:
              email: "ada@example.com"
              body: "<p>Your receipt</p>"
              subject: "Your receipt"
              from_email: "billing@acme.com"
              from_name: "Acme Billing"
      responses:
        "202":
          description: Accepted.
          content:
            application/json:
              schema: { type: object, properties: { data: { type: object, properties: { message_id: { type: string }, status: { type: string } } } } }
              example: { data: { message_id: "9f1c...", status: "queued" } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /exports:
    post:
      tags: [Exports]
      summary: Export a list's contacts (async)
      requestBody:
        required: true
        content: { application/json: { schema: { type: object, required: [list_id], properties: { list_id: { type: integer } } }, example: { list_id: 12 } } }
      responses:
        "202": { description: Accepted; poll the export., content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { export_id: { type: integer }, status: { type: string } } } } }, example: { data: { export_id: 88, status: "queued" } } } } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /exports/{export}:
    parameters: [ { name: export, in: path, required: true, schema: { type: integer } } ]
    get:
      tags: [Exports]
      summary: Export status
      responses:
        "200": { description: The export., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Export' } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /exports/{export}/download:
    parameters: [ { name: export, in: path, required: true, schema: { type: integer } } ]
    get:
      tags: [Exports]
      summary: Download a completed export
      responses:
        "200": { description: The CSV file., content: { text/csv: { schema: { type: string, format: binary } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /operations/{operation}:
    parameters: [ { name: operation, in: path, required: true, schema: { type: integer } } ]
    get:
      tags: [Operations]
      summary: Async operation status
      responses:
        "200": { description: The operation., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Operation' } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhooks
      responses:
        "200": { description: The webhooks., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Webhook' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
    post:
      tags: [Webhooks]
      summary: Create a webhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events: { type: array, items: { type: string }, example: [open, click] }
      responses:
        "201": { description: The webhook (secret shown once)., content: { application/json: { schema: { type: object, properties: { data: { $ref: '#/components/schemas/Webhook' } } } } } }
        "422": { $ref: '#/components/responses/ValidationError' }

  /webhooks/{webhook}/deliveries:
    parameters: [ { name: webhook, in: path, required: true, schema: { type: integer } } ]
    get:
      tags: [Webhooks]
      summary: A webhook's delivery log
      responses:
        "200": { description: The deliveries., content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Delivery' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
        "404": { $ref: '#/components/responses/NotFound' }
