openapi: 3.1.0
info:
  title: '&Open API'
  version: 2026-05
  summary: RESTful JSON API for the &Open gifting platform.
  description: |-
    The &Open API provides programmatic access to the &Open gifting platform. Create and manage gift requests, browse campaigns, and integrate gifting into your workflows.

    ## Authentication
    All requests require a bearer token in the `Authorization` header. Each token is bound to a single tenant — the tenant is derived from the token, so no separate tenant header is required.

    ## Versioning
    The API version is specified via the `AndOpen-API-Version` header. This specification covers version `2026-05`.

    ## Conventions
    - All JSON keys use `snake_case`
    - Resource IDs are UUIDs
    - Timestamps are ISO 8601 with millisecond precision, always UTC
    - Single resource responses are flat top-level JSON objects (no envelope)
    - Collection responses use `{ "data": [...], "has_more": true|false }`
    - Related data is embedded inline when you need it to use the resource — in full, or as a small `{ id, name, … }` summary
    - Otherwise a related resource is referenced by a bare `<resource>_id`, which you fetch separately
    - Error responses are always arrays: `{ "errors": [...] }`
    - Clients must ignore unknown fields and handle unknown enum values
  contact:
    name: '&Open Engineering'
    email: engineering@andopen.co
  license:
    name: Proprietary
servers:
  - url: https://api.andopen.co
    description: Production (US/Global)
  - url: https://api.eu.andopen.co
    description: Production (EU)
  - url: https://api.staging.andopen.co
    description: Staging (for integration testing — non-production data)
security:
  - BearerAuth: []
tags:
  - name: Gift Requests
    description: Create and retrieve gift requests.
  - name: Campaigns
    description: Browse campaigns and their gift catalogues.
paths:
  /gift_requests:
    post:
      operationId: createGiftRequest
      summary: Create a gift request
      description: Create a single gift request for a recipient on a campaign. The campaign drives which gifts are offered, the invitation method, and the responsible user — callers supply only the campaign and the recipient.
      tags:
        - Gift Requests
      parameters:
        - $ref: '#/components/parameters/AndOpenAPIVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateGiftRequest'
            example:
              campaign_id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
              recipient:
                first_name: Alice
                last_name: Smith
                email: alice@example.com
      responses:
        '201':
          description: The newly-created gift request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GiftRequest'
              example:
                id: 4fb4cb3f-9666-43b5-8884-7f5194483d1a
                status: submitted
                campaign_id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                redemption_url: https://gift.andopen.co/r/abc123
                recipient:
                  id: b2c3d4e5-f6a7-8901-bcde-f01234567890
                  first_name: Alice
                  last_name: Smith
                  email: alice@example.com
                shipping_address: null
                line_items: null
                created_at: '2026-04-16T10:30:00.000Z'
                updated_at: '2026-04-16T10:30:00.000Z'
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /campaigns:
    get:
      operationId: listCampaigns
      summary: List campaigns
      description: List the tenant's campaigns as a cursor-paginated collection. Each item carries the full campaign shape — including the deeply nested `products → variants → stock_levels → warehouse` tree — identical to the single-campaign response. Supports filtering by `status` and sorting via `sort`; results default to newest first.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/AndOpenAPIVersion'
        - $ref: '#/components/parameters/CampaignLimit'
        - $ref: '#/components/parameters/PaginationAfter'
        - $ref: '#/components/parameters/CampaignSort'
        - $ref: '#/components/parameters/CampaignStatusFilter'
      responses:
        '200':
          description: A page of campaigns.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CampaignList'
              example:
                data:
                  - id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                    name: Q2 Customer Appreciation
                    description: Send a thank-you gift to our top accounts.
                    status: available
                    archived_at: null
                    products:
                      - id: b2c3d4e5-f6a7-8901-bcde-f01234567890
                        name: Leather Notebook
                        enabled: true
                        variants:
                          - id: c3d4e5f6-a7b8-9012-cdef-012345678901
                            name: Medium / Black
                            sku: NB-LTHR-MED-BLK
                            enabled: true
                            position: 1
                            stock_levels:
                              - id: d4e5f6a7-b8c9-0123-defa-123456789012
                                quantity: 42
                                warehouse:
                                  id: e5f6a7b8-c901-2345-efab-234567890123
                                  name: EU Central — Frankfurt
                                  enabled: true
                                  created_at: '2026-04-16T10:30:00.000Z'
                                  updated_at: '2026-04-16T10:30:00.000Z'
                                created_at: '2026-04-16T10:30:00.000Z'
                                updated_at: '2026-04-16T10:30:00.000Z'
                            created_at: '2026-04-16T10:30:00.000Z'
                            updated_at: '2026-04-16T10:30:00.000Z'
                        created_at: '2026-04-16T10:30:00.000Z'
                        updated_at: '2026-04-16T10:30:00.000Z'
                    created_at: '2026-04-16T10:30:00.000Z'
                    updated_at: '2026-04-16T10:30:00.000Z'
                has_more: true
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /campaigns/{id}:
    get:
      operationId: getCampaign
      summary: Retrieve a campaign
      description: Retrieve a single campaign by ID, with deeply nested owned associations (products → variants → stock_levels → warehouse) always returned inline. When the campaign has a fulfilment warehouse, products / variants / stock levels are filtered to that warehouse; campaigns without a warehouse return the catalogue unfiltered.
      tags:
        - Campaigns
      parameters:
        - $ref: '#/components/parameters/PathId'
        - $ref: '#/components/parameters/AndOpenAPIVersion'
      responses:
        '200':
          description: The campaign.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
              example:
                id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                name: Q2 Customer Appreciation
                description: Send a thank-you gift to our top accounts.
                status: available
                archived_at: null
                products:
                  - id: b2c3d4e5-f6a7-8901-bcde-f01234567890
                    name: Leather Notebook
                    enabled: true
                    variants:
                      - id: c3d4e5f6-a7b8-9012-cdef-012345678901
                        name: Medium / Black
                        sku: NB-LTHR-MED-BLK
                        enabled: true
                        position: 1
                        stock_levels:
                          - id: d4e5f6a7-b8c9-0123-defa-123456789012
                            quantity: 42
                            warehouse:
                              id: e5f6a7b8-c901-2345-efab-234567890123
                              name: EU Central — Frankfurt
                              enabled: true
                              created_at: '2026-04-16T10:30:00.000Z'
                              updated_at: '2026-04-16T10:30:00.000Z'
                            created_at: '2026-04-16T10:30:00.000Z'
                            updated_at: '2026-04-16T10:30:00.000Z'
                        created_at: '2026-04-16T10:30:00.000Z'
                        updated_at: '2026-04-16T10:30:00.000Z'
                    created_at: '2026-04-16T10:30:00.000Z'
                    updated_at: '2026-04-16T10:30:00.000Z'
                created_at: '2026-04-16T10:30:00.000Z'
                updated_at: '2026-04-16T10:30:00.000Z'
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /gift_requests/{id}:
    get:
      operationId: getGiftRequest
      summary: Retrieve a gift request
      description: Retrieve a single gift request by ID, with `recipient`, `shipping_address`, and `line_items` embedded inline. `shipping_address` is null for choice-mode requests; `line_items` is null until items are locked. The campaign is referenced by `campaign_id` only.
      tags:
        - Gift Requests
      parameters:
        - $ref: '#/components/parameters/PathId'
        - $ref: '#/components/parameters/AndOpenAPIVersion'
      responses:
        '200':
          description: The gift request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GiftRequest'
              example:
                id: 4fb4cb3f-9666-43b5-8884-7f5194483d1a
                status: submitted
                campaign_id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
                redemption_url: https://gift.andopen.co/r/abc123
                recipient:
                  id: b2c3d4e5-f6a7-8901-bcde-f01234567890
                  first_name: Alice
                  last_name: Smith
                  email: alice@example.com
                shipping_address: null
                line_items: null
                created_at: '2026-04-16T10:30:00.000Z'
                updated_at: '2026-04-16T10:30:00.000Z'
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: ao-v1
      description: API authentication via bearer token. Tokens follow the format `ao-v1-{stage}-{secret}`, where `{stage}` is the environment the token was issued for (`staging`, `production`) and `{secret}` is an opaque random string. The token is bound to a single tenant — a token cannot perform cross-tenant requests. Include in the `Authorization` header as `Bearer ao-v1-...`.
  schemas:
    GiftRequest:
      type: object
      description: A gift request represents a single gifting transaction — a sender choosing a gift for a recipient, optionally as part of a campaign. The response is a flat top-level JSON object with `recipient`, `shipping_address`, and `line_items` embedded inline; `shipping_address` is null for choice-mode, `line_items` is null until items are locked.
      required:
        - id
        - status
        - redemption_url
        - recipient
        - shipping_address
        - line_items
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the gift request.
          examples:
            - 4fb4cb3f-9666-43b5-8884-7f5194483d1a
        status:
          type: string
          description: The public-facing status of the gift request. Maps internal fulfilment states to a coarser-grained set suitable for API consumers.
          enum:
            - submitted
            - redeemed
            - dispatched
            - delivered
            - cancelled
          examples:
            - submitted
        campaign_id:
          type:
            - string
            - 'null'
          format: uuid
          description: ID of the campaign this gift request belongs to, or null for ad-hoc (non-campaign) sends. Fetch the full campaign via `GET /campaigns/{id}`.
          examples:
            - a1b2c3d4-e5f6-7890-abcd-ef1234567890
        redemption_url:
          type:
            - string
            - 'null'
          format: uri
          description: URL where the recipient can redeem the gift. Null if the gift request has not yet generated a redemption link.
          examples:
            - https://gift.andopen.co/r/abc123
        recipient:
          $ref: '#/components/schemas/Recipient'
        shipping_address:
          oneOf:
            - $ref: '#/components/schemas/ShippingAddress'
            - type: 'null'
          description: The recipient's shipping address, returned inline when populated. Populated when an address was supplied at creation (direct-send mode); null otherwise (choice-mode). Never changes over the gift request's lifecycle.
        line_items:
          type:
            - array
            - 'null'
          description: 'The line items included in this gift request, returned inline when populated. Returned as `null` (never as `[]`) when items are not yet locked: for direct-send the items lock at creation, so this is populated in every status; for choice-mode the items lock at redemption.'
          items:
            $ref: '#/components/schemas/LineItem'
        created_at:
          type: string
          format: date-time
          description: When the gift request was created. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
        updated_at:
          type: string
          format: date-time
          description: When the gift request was last updated. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
    CreateGiftRequest:
      type: object
      description: 'Request body for creating a gift request. The payload identifies the campaign and the recipient. Two modes are supported, chosen by which fields are present: choice-mode (no `shipping_address`, no `line_items`) derives the offered gifts server-side from the campaign''s configured variants; direct-send mode (`shipping_address` and `line_items` both present) ships the supplied items to the supplied address. `shipping_address` and `line_items` are required together — supplying one without the other is rejected. Direct-send additionally requires the campaign''s `invitation_method` to be `sender_provided_address`.'
      required:
        - campaign_id
        - recipient
      properties:
        campaign_id:
          type: string
          format: uuid
          description: ID of the campaign this gift request belongs to. The campaign drives which gifts are offered, the invitation method, the theme, and the responsible user.
          examples:
            - a1b2c3d4-e5f6-7890-abcd-ef1234567890
        recipient:
          type: object
          description: The gift recipient, supplied inline by value. Recipients do not have an independent endpoint. `email` is optional.
          required:
            - first_name
            - last_name
          properties:
            first_name:
              type: string
              description: Recipient's first name.
              examples:
                - Alice
            last_name:
              type: string
              description: Recipient's last name.
              examples:
                - Smith
            email:
              type:
                - string
                - 'null'
              format: email
              description: Recipient's email address. Optional.
              examples:
                - alice@example.com
        shipping_address:
          type: object
          description: The recipient's shipping address. Required (along with `line_items`) to create a direct-send gift request; omit both for choice-mode. Explicit `null` is rejected. The `country_code` must be ISO 3166-1 alpha-2 in uppercase; lowercase is rejected. Does not accept name fields — the shipping label uses `recipient.first_name` and `recipient.last_name`.
          required:
            - address1
            - city
            - region
            - postal_code
            - country_code
            - phone
          properties:
            address1:
              type: string
              description: First line of the street address.
              examples:
                - 12 Merrion Square
            address2:
              type:
                - string
                - 'null'
              description: Second line of the street address (apartment, suite, etc.).
              examples:
                - Apt 4B
            city:
              type: string
              description: City or locality.
              examples:
                - Dublin
            region:
              type: string
              description: State, province, or region within the country.
              examples:
                - Leinster
            postal_code:
              type: string
              description: Postal or ZIP code.
              examples:
                - D02 XY45
            country_code:
              type: string
              pattern: ^[A-Z]{2}$
              description: ISO 3166-1 alpha-2 country code in uppercase.
              examples:
                - IE
            phone:
              type: string
              description: Recipient's contact phone number. Must be a valid phone number; it need not belong to the destination country. For a number outside the destination country, use E.164 (leading `+`, preferred) or the `00` international prefix; a number with no international prefix is validated against the destination country.
              examples:
                - '+353871234567'
        line_items:
          type: array
          description: The line items to ship. Required (along with `shipping_address`) to create a direct-send gift request; omit both for choice-mode. Explicit `null` and empty arrays are rejected. Each item must identify a single enabled tenant variant via `sku`, `variant_id`, or both; supplying both is accepted only if they resolve to the same variant.
          minItems: 1
          items:
            type: object
            description: A single line item — one product variant and a quantity.
            required:
              - quantity
            anyOf:
              - required:
                  - sku
              - required:
                  - variant_id
            properties:
              sku:
                type: string
                description: Stock-keeping unit of the product variant. At least one of `sku` or `variant_id` must be supplied per item.
                examples:
                  - HOODIE-L-BLK
              variant_id:
                type: string
                format: uuid
                description: '&Open variant identifier. At least one of `sku` or `variant_id` must be supplied per item.'
                examples:
                  - c3d4e5f6-a7b8-9012-cdef-012345678901
              quantity:
                type: integer
                minimum: 1
                description: How many of this variant to ship in this line item.
                examples:
                  - 1
    Recipient:
      type: object
      description: A gift recipient, always returned inline within a gift request response. Recipients belong to a single gift request and have no independent endpoint. Name and email fields are optional.
      required:
        - id
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the recipient.
          examples:
            - b2c3d4e5-f6a7-8901-bcde-f01234567890
        first_name:
          type:
            - string
            - 'null'
          description: Recipient's first name.
          examples:
            - Alice
        last_name:
          type:
            - string
            - 'null'
          description: Recipient's last name.
          examples:
            - Smith
        email:
          type:
            - string
            - 'null'
          format: email
          description: Recipient's email address.
          examples:
            - alice@example.com
    ShippingAddress:
      type: object
      description: A shipping address on a gift request, always returned inline within a gift request response. Shipping addresses belong to a single gift request and have no independent endpoint. Populated when an address was supplied at creation (direct-send mode); never changes over the lifecycle.
      required:
        - id
        - first_name
        - last_name
        - address1
        - city
        - region
        - postal_code
        - country_code
        - phone
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the shipping address.
          examples:
            - c3d4e5f6-a7b8-9012-cdef-012345678901
        first_name:
          type: string
          description: Recipient's first name as it should appear on the shipping label.
          examples:
            - Alice
        last_name:
          type: string
          description: Recipient's last name as it should appear on the shipping label.
          examples:
            - Smith
        address1:
          type: string
          description: First line of the street address.
          examples:
            - 12 Merrion Square
        address2:
          type:
            - string
            - 'null'
          description: Second line of the street address (apartment, suite, etc.).
          examples:
            - Apt 4B
        city:
          type: string
          description: City or locality.
          examples:
            - Dublin
        region:
          type: string
          description: State, province, or region within the country.
          examples:
            - Leinster
        postal_code:
          type: string
          description: Postal or ZIP code.
          examples:
            - D02 XY45
        country_code:
          type: string
          pattern: ^[A-Z]{2}$
          description: ISO 3166-1 alpha-2 country code in uppercase.
          examples:
            - IE
        phone:
          type: string
          description: Recipient's contact phone number. Must be a valid phone number; it need not belong to the destination country. For a number outside the destination country, use E.164 (leading `+`, preferred) or the `00` international prefix; a number with no international prefix is validated against the destination country.
          examples:
            - '+353871234567'
    LineItem:
      type: object
      description: A line item on a gift request, always returned inline within a gift request response. Line items belong to a single gift request and have no independent endpoint.
      required:
        - id
        - name
        - quantity
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the line item.
          examples:
            - d4e5f6a7-b8c9-0123-4567-890abcdef012
        sku:
          type:
            - string
            - 'null'
          description: Stock-keeping unit of the line item's product variant. Nullable independently of `variant_id`.
          examples:
            - HOODIE-L-BLK
        variant_id:
          type:
            - string
            - 'null'
          format: uuid
          description: '&Open variant identifier of the line item''s product variant. Nullable independently of `sku`.'
          examples:
            - c3d4e5f6-a7b8-9012-cdef-012345678901
        name:
          type: string
          description: 'Human-readable display name. Single-variant products: the product name. Multi-variant products: the product name and the variant name joined with `: `.'
          examples:
            - 'Hoodie: Large / Black'
        quantity:
          type: integer
          minimum: 1
          description: How many of this variant are included in the line item.
          examples:
            - 1
    Campaign:
      type: object
      description: |-
        A campaign groups together the gift catalogue offered to recipients on a given send. The response is a flat top-level JSON object with the deeply nested catalogue (products → variants → stock_levels → warehouse) embedded inline.

        When the campaign has a fulfilment warehouse, the catalogue is filtered to that warehouse: products, variants, and stock levels with no stock at the campaign's warehouse are omitted, and each variant exposes a single stock level scoped to that warehouse. Campaigns without a warehouse return the catalogue unfiltered.
      required:
        - id
        - name
        - description
        - status
        - archived_at
        - products
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the campaign.
          examples:
            - a1b2c3d4-e5f6-7890-abcd-ef1234567890
        name:
          type: string
          description: Display name of the campaign.
          examples:
            - Q2 Customer Appreciation
        description:
          type:
            - string
            - 'null'
          description: Long-form description of the campaign. Always present in the response; null when no description has been set.
          examples:
            - Send a thank-you gift to our top accounts.
        status:
          type: string
          description: Public-facing status of the campaign. Internal hidden states are collapsed to `available` for API consumers.
          enum:
            - available
            - archived
          examples:
            - available
        archived_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When the campaign was archived. Null if the campaign has not been archived. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
        products:
          type: array
          description: Products available to recipients on this campaign. Owned association — always returned inline. Filtered to the campaign's warehouse when one is set.
          items:
            $ref: '#/components/schemas/Product'
        created_at:
          type: string
          format: date-time
          description: When the campaign was created. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
        updated_at:
          type: string
          format: date-time
          description: When the campaign was last updated. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
    CampaignList:
      type: object
      description: A page of campaigns. Collection responses wrap the items in `data` and signal whether further pages exist with `has_more`. Each item carries the full campaign shape, identical to the single-campaign response.
      required:
        - data
        - has_more
      properties:
        data:
          type: array
          description: The campaigns in this page.
          items:
            $ref: '#/components/schemas/Campaign'
        has_more:
          type: boolean
          description: Whether more campaigns exist beyond this page. When true, fetch the next page by passing the last item's `id` as the `after` cursor.
          examples:
            - true
    Product:
      type: object
      description: A product within a campaign, always returned inline within a campaign response. Products belong to one or more campaigns and have no independent endpoint in this API version.
      required:
        - id
        - name
        - enabled
        - variants
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the product.
          examples:
            - b2c3d4e5-f6a7-8901-bcde-f01234567890
        name:
          type: string
          description: Display name of the product, in the tenant's default locale.
          examples:
            - Leather Notebook
        enabled:
          type: boolean
          description: Whether the product is enabled for fulfilment.
          examples:
            - true
        variants:
          type: array
          description: Variants of this product, always returned inline, ordered by `position` ascending. Filtered to the campaign's warehouse when one is set.
          items:
            $ref: '#/components/schemas/Variant'
        created_at:
          type: string
          format: date-time
          description: When the product was created. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
        updated_at:
          type: string
          format: date-time
          description: When the product was last updated. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
    Variant:
      type: object
      description: A variant of a product (e.g. size or colour), always returned inline within a product. Variants have no independent endpoint in this API version.
      required:
        - id
        - name
        - sku
        - enabled
        - position
        - stock_levels
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the variant.
          examples:
            - c3d4e5f6-a7b8-9012-cdef-012345678901
        name:
          type:
            - string
            - 'null'
          description: Display name of the variant. Null when the variant inherits the product name (single-variant products often have no own name).
          examples:
            - Medium / Black
        sku:
          type:
            - string
            - 'null'
          description: Stock-keeping unit identifier for the variant.
          examples:
            - NB-LTHR-MED-BLK
        enabled:
          type: boolean
          description: Whether the variant is enabled for fulfilment.
          examples:
            - true
        position:
          type: integer
          description: Display order of the variant within its product. Lower values appear first.
          examples:
            - 1
        stock_levels:
          type: array
          description: Stock levels for this variant, always returned inline. Filtered to the campaign's warehouse when one is set, in which case the array contains at most one entry per the `(variant_id, warehouse_id)` uniqueness constraint.
          items:
            $ref: '#/components/schemas/StockLevel'
        created_at:
          type: string
          format: date-time
          description: When the variant was created. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
        updated_at:
          type: string
          format: date-time
          description: When the variant was last updated. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
    StockLevel:
      type: object
      description: Stock available for a variant at a single warehouse, always returned inline within a variant.
      required:
        - id
        - quantity
        - warehouse
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the stock level.
          examples:
            - d4e5f6a7-b8c9-0123-defa-123456789012
        quantity:
          type: integer
          description: Net units fulfillable for the variant at this warehouse. When the product has pending stock enabled this nets expected (pending) inflow against backordered units; otherwise it is the units currently on hand. Always positive for a stock level that appears in a response — it is the figure that decides whether the variant is in stock.
          examples:
            - 42
        warehouse:
          $ref: '#/components/schemas/Warehouse'
        created_at:
          type: string
          format: date-time
          description: When the stock level was first recorded. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
        updated_at:
          type: string
          format: date-time
          description: When the stock level was last updated. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
    Warehouse:
      type: object
      description: A fulfilment warehouse holding stock for one or more variants. Owned association — always inline within a stock level.
      required:
        - id
        - name
        - enabled
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the warehouse.
          examples:
            - e5f6a7b8-c901-2345-efab-234567890123
        name:
          type: string
          description: Display name of the warehouse.
          examples:
            - EU Central — Frankfurt
        enabled:
          type: boolean
          description: Whether the warehouse is currently enabled for fulfilment.
          examples:
            - true
        created_at:
          type: string
          format: date-time
          description: When the warehouse was created. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
        updated_at:
          type: string
          format: date-time
          description: When the warehouse was last updated. ISO 8601, millisecond precision, UTC.
          examples:
            - '2026-04-16T10:30:00.000Z'
    Error:
      type: object
      description: A single error object within an error response.
      required:
        - type
        - code
        - message
      properties:
        type:
          type: string
          description: Error category.
          enum:
            - validation_error
            - authentication_error
            - authorization_error
            - not_found_error
            - rate_limit_error
            - api_error
          examples:
            - validation_error
        code:
          type: string
          pattern: ^[a-z][a-z0-9_]*$
          description: |
            Machine-readable error code, scoped to the error `type`. Open enum —
            new codes may be added without breaking the spec. Clients must handle
            unknown codes gracefully, typically by falling back to `type` or
            `message`. Codes are always snake_case.

            Known codes by type:
            - `validation_error`: `required`, `invalid`, `invalid_format`, `invalid_reference`, `not_unique`, `out_of_stock`
            - `authentication_error`: `invalid_token`, `expired_token`, `missing_token`
            - `authorization_error`: `forbidden`, `insufficient_scope`
            - `not_found_error`: `not_found`
            - `rate_limit_error`: `rate_limited`
            - `api_error`: `internal_error`, `malformed_request`, `service_unavailable`
          examples:
            - required
        message:
          type: string
          description: Human-readable error description.
          examples:
            - is required
        param:
          type: string
          description: The request parameter that caused the error, using dot notation for nested fields. Omitted for errors not tied to a specific field.
          examples:
            - recipient.email
    ErrorResponse:
      type: object
      description: Error response body. All error responses use this shape — an array of error objects, even for single errors.
      required:
        - errors
      properties:
        errors:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/Error'
  parameters:
    PathId:
      name: id
      in: path
      required: true
      description: Unique identifier (UUID) of the resource.
      schema:
        type: string
        format: uuid
        examples:
          - 4fb4cb3f-9666-43b5-8884-7f5194483d1a
    AndOpenAPIVersion:
      name: AndOpen-API-Version
      in: header
      required: true
      description: API version. This specification covers version `2026-05`. Required on all requests.
      schema:
        type: string
        enum:
          - 2026-05
        examples:
          - 2026-05
    CampaignLimit:
      name: limit
      in: query
      required: false
      description: Maximum number of campaigns to return in a single page. Defaults to 25, which is also the cap. Larger values are clamped to 25 server-side; a non-numeric, zero, or negative value is rejected with a 400.
      schema:
        type: integer
        minimum: 1
        maximum: 25
        default: 25
        examples:
          - 25
    PaginationAfter:
      name: after
      in: query
      required: false
      description: Keyset pagination cursor. Pass the `id` of the last item from the previous page to fetch the page after it; omit it to fetch the first page. A value that does not reference an existing record is rejected with a 400.
      schema:
        type: string
        format: uuid
        examples:
          - a1b2c3d4-e5f6-7890-abcd-ef1234567890
    CampaignSort:
      name: sort
      in: query
      required: false
      description: Field to sort the campaigns by. Prefix the field with `-` for descending order. Defaults to `-created_at` (newest first). An unsupported field is rejected with a 400.
      schema:
        type: string
        enum:
          - created_at
          - '-created_at'
          - name
          - '-name'
        default: '-created_at'
        examples:
          - '-created_at'
    CampaignStatusFilter:
      name: status
      in: query
      required: false
      description: Filter campaigns by public status. Accepts a single value (`available` or `archived`) or a comma-separated list of both. An unrecognised value matches nothing and yields an empty page.
      schema:
        type: string
        examples:
          - available
          - available,archived
  responses:
    BadRequest:
      description: The request is malformed or violates a precondition. Typical triggers are a missing or unsupported `AndOpen-API-Version` header, a request body that is not valid JSON, or pagination parameters that fail server-side validation (`limit` non-numeric or non-positive, `after` referencing a record that does not exist).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            errors:
              - type: api_error
                code: unsupported_api_version
                message: 'Requests must include header ''AndOpen-API-Version: 2026-05''.'
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
    Unauthorized:
      description: Authentication failed — missing or invalid bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            errors:
              - type: authentication_error
                code: invalid_token
                message: The access token provided is invalid or has expired.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
    Forbidden:
      description: Authorised but not permitted to access this resource.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            errors:
              - type: authorization_error
                code: forbidden
                message: You do not have permission to access this resource.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
    NotFound:
      description: The requested resource does not exist.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            errors:
              - type: not_found_error
                code: not_found
                message: The requested resource could not be found.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
    UnprocessableEntity:
      description: Validation error — the request was parseable but contains invalid data. All validation errors are returned at once.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            errors:
              - type: validation_error
                code: required
                message: is required
                param: recipient.email
              - type: validation_error
                code: invalid_reference
                message: does not exist
                param: campaign_id
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
    RateLimited:
      description: Too many requests — rate limit exceeded.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            errors:
              - type: rate_limit_error
                code: rate_limited
                message: Rate limit exceeded. Retry after the window resets.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
    InternalError:
      description: Internal server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            errors:
              - type: api_error
                code: internal_error
                message: An unexpected error occurred. Please try again later.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
    ServiceUnavailable:
      description: Service unavailable — the tenant may be under maintenance. Check the `Retry-After` header.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            errors:
              - type: api_error
                code: service_unavailable
                message: The service is temporarily unavailable. Please try again later.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
  headers:
    RequestId:
      description: Unique identifier for the request.
      schema:
        type: string
        format: uuid
        examples:
          - a1b2c3d4-e5f6-7890-abcd-ef1234567890
    RateLimitLimit:
      description: The maximum number of requests allowed in the current window.
      schema:
        type: integer
        examples:
          - 100
    RateLimitRemaining:
      description: The number of requests remaining in the current window.
      schema:
        type: integer
        examples:
          - 97
    RateLimitReset:
      description: 'The absolute time at which the current rate limit window resets, as a Unix timestamp (seconds since epoch). Note: this is an absolute timestamp, not delta-seconds — use `Retry-After` (delta-seconds) when scheduling a retry.'
      schema:
        type: integer
        examples:
          - 1714060800
    RetryAfter:
      description: Number of seconds to wait before retrying the request.
      schema:
        type: integer
        examples:
          - 30
