openapi: 3.0.3
info:
  title: Send Tim API
  version: "1"
  description: |
    Public REST API for Send Tim. Every request is scoped to the workspace
    that owns the API key used to authenticate it — there is no cross-workspace
    access and no separate scopes: a key can do anything a dashboard admin can.
servers:
  - url: https://sendtim.com/api/v1
security:
  - ApiKeyAuth: []
components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: 'Authorization: Bearer <api key>'
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum: [unauthenticated, forbidden, not_found, validation_failed, conflict, rate_limited, internal]
            message:
              type: string
            details:
              type: object
paths:
  /contacts:
    get:
      summary: List contacts
      parameters:
        - name: search
          in: query
          schema: { type: string }
        - name: status
          in: query
          schema: { type: string, enum: [subscribed, unsubscribed, bounced, complained] }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: pageSize
          in: query
          schema: { type: integer, default: 25, maximum: 100 }
      responses:
        '200': { description: OK }
        '429': { description: Rate limited, $ref: '#/components/schemas/Error' }
    post:
      summary: Create a contact
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                firstName: { type: string }
                lastName: { type: string }
                listIds: { type: array, items: { type: string, format: uuid } }
                consentGiven: { type: boolean, default: false }
      responses:
        '201': { description: Created }
        '403': { description: Over the plan's contact limit }
        '409': { description: A contact with this email already exists }
  /contacts/{id}:
    get:
      summary: Get a contact
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }
        '404': { description: Not found }
    patch:
      summary: Update a contact
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }
        '404': { description: Not found }
    delete:
      summary: Delete a contact (soft delete)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }
        '404': { description: Not found }
  /lists:
    get:
      summary: List lists
      responses:
        '200': { description: OK }
    post:
      summary: Create a list
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                description: { type: string }
                doubleOptIn: { type: boolean, default: false }
      responses:
        '201': { description: Created }
  /lists/{id}/contacts:
    post:
      summary: Add existing contacts to a list
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactIds]
              properties:
                contactIds: { type: array, items: { type: string, format: uuid }, maxItems: 500 }
      responses:
        '200': { description: OK }
        '404': { description: List not found }
  /campaigns:
    get:
      summary: List campaigns
      responses:
        '200': { description: OK }
    post:
      summary: Create a campaign, optionally sending it immediately
      description: |
        Sending (`send`) requires the Starter plan or above. `send.confirmationText`
        must exactly match the campaign's `name` — the same safeguard the
        dashboard's "type the campaign name to confirm" step enforces.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, subject, fromName, fromEmail, audience]
              properties:
                name: { type: string }
                subject: { type: string }
                fromName: { type: string }
                fromEmail: { type: string, format: email }
                audience:
                  type: object
                  properties:
                    listIds: { type: array, items: { type: string, format: uuid } }
                    segmentIds: { type: array, items: { type: string, format: uuid } }
                send:
                  type: object
                  description: Omit to create a draft only.
                  properties:
                    confirmationText: { type: string }
                    acknowledgedRecipientCount: { type: integer }
      responses:
        '201': { description: Created (and sending, if `send` was provided) }
        '403': { description: Plan does not include API campaign sending }
  /automations/{id}/trigger:
    post:
      summary: Manually trigger an automation for one contact
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactId]
              properties:
                contactId: { type: string, format: uuid }
      responses:
        '200': { description: OK }
        '400': { description: Automation not active/manual, or contact not in this workspace }
