> ## Documentation Index
> Fetch the complete documentation index at: https://developers.safarapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a booking

> Creates a booking in `CONFIRMED` status from a valid quote. The booking is
**confirmed immediately** (no PENDING/24h window) — payment is recorded via the
`payment_confirmation` fields.

The traveler (`traveler` field) is resolved / created as a **Keycloak shadow user**
on the Safariat side (see ADR-018). The PDF voucher and confirmation email are sent
directly to the traveler at the provided email address.

## Idempotency
The `Idempotency-Key` header is **required**. Same key + same body → the cached
response is returned. Same key + different body → `409 Conflict`. Retained for 24 h.

## Payment validation
`payment_confirmation.paid_amount_net` **must equal** the
`quote.pricing.amount_due_to_safariat.amount` returned by `POST /quotes` (anti-fraud).
Otherwise → `422 payment.amount.mismatch`.


## Request Structure

```json expandable
{
  "data": {
    "type": "bookings",
    "attributes": {
      "account_id": 1,
      "location_id": 10,
      "consignment_reference": "CONS-2024-001",
      "direction": "inbound",
      "hazardous": false,
      "start_time": "2024-09-15T08:00:00Z",
      "end_time": "2024-09-15T09:00:00Z",
      "haulier": "DHL Express",
      "intake_type": "full_load",
      "container_number": "MSKU1234567",
      "storage_unit_quantity": 20,
      "notes": "Fragile goods - handle with care"
    }
  }
}
```

## Required Fields

| Field                   | Type     | Description                                                       |
| ----------------------- | -------- | ----------------------------------------------------------------- |
| `account_id`            | integer  | Stock account ID (owner of the goods)                             |
| `location_id`           | integer  | Dock location ID (must have `available_in_booking_diary` enabled) |
| `consignment_reference` | string   | Consignment reference number                                      |
| `direction`             | string   | One of: `inbound`, `outbound`                                     |
| `hazardous`             | boolean  | Whether the goods are hazardous                                   |
| `start_time`            | datetime | Booking start time (ISO 8601)                                     |
| `end_time`              | datetime | Booking end time (must be after start\_time)                      |

## Optional Fields

| Field                   | Type    | Description                                                                                  |
| ----------------------- | ------- | -------------------------------------------------------------------------------------------- |
| `account_reference`     | string  | Account's own reference                                                                      |
| `booking_reference`     | string  | Booking reference number                                                                     |
| `container_number`      | string  | Container number (for container intake)                                                      |
| `haulier`               | string  | Haulier/carrier name                                                                         |
| `intake_type`           | string  | One of: `container`, `part_load`, `full_load`                                                |
| `notes`                 | string  | Free text notes                                                                              |
| `status`                | string  | One of: `pending`, `arrived`, `departed`, `fail_to_arrive`, `cancelled` (default: `pending`) |
| `storage_unit_quantity` | integer | Expected number of storage units (e.g., pallets)                                             |

## Slot Availability Validation

The system validates that the requested time slot is available before creating the booking.

### Opening Times Compliance

The location (or its parent warehouse) may have opening times configured. Opening hours vary by day of week, so the system checks the specific day's hours for both start and end times.

**Error Response (code 295):**

```json
{
  "errors": [
    {
      "code": 295,
      "symbol": "outside_of_opening_times",
      "details": "Provided start time is outside of opening times",
      "source": "start_time"
    }
  ]
}
```

### No Overlapping Bookings

The system uses time range overlap detection to ensure no other booking exists at the same location during the requested period.

**Error Response (code 296):**

```json
{
  "errors": [
    {
      "code": 296,
      "symbol": "overlapping_bookings",
      "details": "There are overlapping bookings for the selected time slot",
      "source": "time_slot"
    }
  ]
}
```

## Finding Available Slots Workflow

To find an available time slot before creating a booking:

1. **Query eligible locations** - Find locations where `available_in_booking_diary` is true (via GraphQL)
2. **Query existing bookings** - Retrieve bookings for the target date range and location (via GraphQL)
3. **Calculate gaps** - Identify time gaps that fall within the location's opening hours for the target day of week
4. **Create booking** - POST /bookings with the chosen slot


## OpenAPI

````yaml /api-reference/openapi.yaml post /bookings
openapi: 3.1.0
info:
  title: SafarAPI
  version: 1.0.0
  summary: B2B API for banking partners — Safariat catalog and booking
  description: >
    SafarAPI lets banking partners integrate the Safariat travel catalog into
    their

    authenticated customer areas and book on behalf of their own end customers
    who are

    already authenticated and KYC-verified on the bank side.


    ## Channel model

    - The traveler never authenticates with Safariat.

    - Payment is collected on the banking platform (out-of-band from Safariat).

    - The bank submits the HMAC-signed payment confirmation at booking time.

    - Monthly post-payment settlement (see `GET /settlements`).


    ## Authentication

    Bearer API key in the `Authorization` header. Format
    `sk_live_<prefix>_<secret>` (production)

    or `sk_test_<prefix>_<secret>` (sandbox). The secret is shown only once at
    key generation —

    Safariat stores only an argon2id hash.


    ## Write request signing

    Production write requests (`POST`, `PUT`, `DELETE`) additionally require:

    - `X-Timestamp`: Unix timestamp in seconds, validity window ±5 minutes.

    - `X-Signature`: `hex(HMAC_SHA256(secret,
    "{timestamp}\n{method}\n{path}\n{body}"))`.

    - `Idempotency-Key`: opaque string unique per operation (UUID v4
    recommended),
      retained for 24 h. Replayed responses carry an `Idempotent-Replayed: true` header.

    Request signing applies only to production `sk_live_*` keys. Sandbox
    `sk_test_*` keys are

    exempt from request signing: `X-Signature` and `X-Timestamp` are not
    required for writes

    in the sandbox (so the developer-portal "Try it" playground works end to
    end).

    `Idempotency-Key` is still required on writes in both environments.


    ## Error format

    All errors follow the `Error` schema with a stable i18n code, an English
    message, and the

    `request_id` for correlation with Safariat logs.


    ## Versioning

    URL-based versioning (`/v1`). No breaking changes within a version. Removals
    are announced

    6 months in advance via the `Sunset` header (RFC 8594).


    ## Rate limiting

    Default limit of 600 req/min per key, contractually adjustable. The
    `X-RateLimit-Limit`,

    `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers are returned on
    every response.

    Exceeding the limit → `429 Too Many Requests` plus the `Retry-After` header.


    ## Sandbox

    An `sk_test_*` key targets the same infrastructure as production but
    isolates bookings in

    `test_mode=true`: no real email is sent, no settlement is issued, and
    webhooks are marked

    `test: true`.
  contact:
    name: SafarAPI Team
    email: api@safariat.ma
    url: https://developers.safariat.ma
  license:
    name: Proprietary — use subject to a signed partner agreement
  termsOfService: https://safariat.ma/legal/partner-terms
servers:
  - url: https://api.safarapi.com/api/partner/v1
    description: >-
      Production and sandbox share this host. The environment is selected by the
      API key prefix: sk_live_ targets production, sk_test_ targets the sandbox.
security:
  - apiKey: []
tags:
  - name: Meta
    description: Metadata for the current key, service health.
  - name: Catalog
    description: Browsing the Safariat adventure catalog.
  - name: Quotes
    description: Price-locked quotes (30 min TTL) ahead of booking.
  - name: Bookings
    description: Creating, retrieving, and cancelling bookings.
  - name: Settlements
    description: Monthly Safariat → partner payout invoices.
  - name: Webhooks
    description: Managing endpoints that receive Safariat events.
  - name: Team
    description: Members of the partner console workspace and their roles.
  - name: Reviews
    description: Read-only access to traveler reviews on the Safariat catalog.
  - name: Files
    description: Presigned uploads for partner-supplied files (currently review photos).
paths:
  /bookings:
    post:
      tags:
        - Bookings
      summary: Create a booking
      description: >
        Creates a booking in `CONFIRMED` status from a valid quote. The booking
        is

        **confirmed immediately** (no PENDING/24h window) — payment is recorded
        via the

        `payment_confirmation` fields.


        The traveler (`traveler` field) is resolved / created as a **Keycloak
        shadow user**

        on the Safariat side (see ADR-018). The PDF voucher and confirmation
        email are sent

        directly to the traveler at the provided email address.


        ## Idempotency

        The `Idempotency-Key` header is **required**. Same key + same body → the
        cached

        response is returned. Same key + different body → `409 Conflict`.
        Retained for 24 h.


        ## Payment validation

        `payment_confirmation.paid_amount_net` **must equal** the

        `quote.pricing.amount_due_to_safariat.amount` returned by `POST /quotes`
        (anti-fraud).

        Otherwise → `422 payment.amount.mismatch`.
      operationId: createBooking
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XTimestamp'
        - $ref: '#/components/parameters/XSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBookingRequest'
      responses:
        '201':
          description: Booking created and confirmed
          headers:
            Location:
              schema:
                type: string
              description: Absolute URL of the created booking.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Booking'
              example:
                booking_number: MV-AB7X92
                partner_reference: YBANK-BOOK-001
                status: CONFIRMED
                adventure_slug: marrakech-3-jours-desert-sandbox
                adventure_title: '[DEMO] 3-day Sahara desert circuit from Marrakech'
                travel_date: '2026-07-12'
                traveler_name: A. B.
                pricing:
                  b2c_price:
                    amount: '5250.00'
                    currency: MAD
                  customer_price:
                    amount: '6210.00'
                    currency: MAD
                  amount_due_to_safariat:
                    amount: '5162.50'
                    currency: MAD
                  awb_commission_share:
                    amount: '87.50'
                    currency: MAD
                  sandbox_surcharge:
                    amount: '960.00'
                    currency: MAD
                  indicative: false
                settlement_id: null
                created_at: '2026-07-12T10:05:00Z'
                traveler:
                  first_name: Amina
                  last_name: Benali
                  email: amina.benali@example.com
                  phone: '+212600112233'
                  language: fr
                  customer_reference: YBANK-CUST-77310
                rooms_allocation:
                  - room_index: 0
                    occupants:
                      - type: ADULT
                      - type: ADULT
                      - type: CHILD
                        age: 8
                      - type: CHILD
                        age: 12
                special_requests: null
                payment_confirmation:
                  bank_payment_ref: YBANK-PAY-2026-07-12-00891
                  paid_at: '2026-07-12T10:04:30Z'
                  payment_method: ACCOUNT_DEBIT
                snapshot_id: 5e6d7c8b-9a0f-4b1c-8d2e-3f4a5b6c7d8e
                cancellation_tiers:
                  - days_before: 30
                    refund_percent: 100
                    refundable_amount:
                      amount: '5400.00'
                      currency: MAD
                  - days_before: 14
                    refund_percent: 50
                    refundable_amount:
                      amount: '2700.00'
                      currency: MAD
                  - days_before: 7
                    refund_percent: 0
                    refundable_amount:
                      amount: '0.00'
                      currency: MAD
                voucher_url: https://placehold.co/600x400?text=DEMO
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Partner credit limit would be exceeded, or booking for a traveler
            whose identity is already linked to another partner under dispute.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                creditLimitExceeded:
                  value:
                    code: partner.credit.limit.exceeded
                    message: Outstanding amount would exceed the partner credit limit.
                    request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
                    details:
                      credit_limit:
                        amount: '50000.00'
                        currency: MAD
                      outstanding:
                        amount: '48200.00'
                        currency: MAD
                      this_booking:
                        amount: '3200.00'
                        currency: MAD
        '404':
          description: Unknown or expired quote.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: adventure.not.found
                message: >-
                  The adventure referenced by the quote no longer exists or is
                  not available to this partner.
                request_id: req_8f3a1c9e2b7d40
        '409':
          description: >-
            Conflict — no booking created. Either the `Idempotency-Key` was
            reused with a different body, or a uniqueness guard fired (ADR-024):
            quote already used (`partner.booking.quote.already.used`), bank
            payment reference already used (`payment.confirmation.replayed`), or
            duplicate `partner_reference`
            (`partner.booking.partnerreference.duplicate`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: partner.booking.quote.already.used
                message: This quote has already been used for a booking.
                request_id: req_8f3a1c9e2b7d40
        '422':
          description: >-
            Business rule violated. The slot is re-validated at booking time
            (the quote may be up to 30 min stale):
            `adventure.capacity.exhausted` (no capacity left),
            `partner.booking.cutoff.too.late` (cut-off passed),
            `payment.amount.mismatch` / `payment.amount.total.invalid`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                amountMismatch:
                  value:
                    code: payment.amount.mismatch
                    message: >-
                      payment_confirmation.paid_amount_net does not match
                      quote.pricing.amount_due_to_safariat.
                    request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
                capacityExhausted:
                  value:
                    code: adventure.capacity.exhausted
                    message: No remaining capacity for this slot.
                    request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHD
        '429':
          $ref: '#/components/responses/TooManyRequests'
components:
  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema:
        type: string
        maxLength: 255
      description: >
        Opaque, client-generated string unique per logical operation (a UUID v4
        is

        recommended but any non-blank value up to 255 chars is accepted). Same
        key +

        same body = the cached response is returned with the
        `Idempotent-Replayed: true`

        header. Same key + different body = `409 Conflict`. Retained for 24 h.
    XTimestamp:
      in: header
      name: X-Timestamp
      required: true
      schema:
        type: integer
        format: int64
      description: >
        Unix timestamp in seconds at the time the request is issued. Validity
        window

        ±5 minutes — beyond that the request is rejected. Required for
        production

        `sk_live_*` keys only; not required for sandbox `sk_test_*` keys.
    XSignature:
      in: header
      name: X-Signature
      required: true
      schema:
        type: string
        pattern: ^[a-f0-9]{64}$
      description: >
        `hex(HMAC_SHA256(secret, "{X-Timestamp}\n{METHOD}\n{path}\n{body}"))`.

        The `path` includes the query string. The `body` is the exact JSON
        representation

        sent — any reformatting invalidates the signature. Required for
        production

        `sk_live_*` keys only; not required for sandbox `sk_test_*` keys.
  schemas:
    CreateBookingRequest:
      type: object
      required:
        - quote_id
        - traveler
        - rooms_allocation
        - partner_reference
        - payment_confirmation
      properties:
        quote_id:
          type: string
          format: uuid
        partner_reference:
          type: string
          minLength: 1
          maxLength: 100
          description: >
            The bank's internal reference (booking ID on the bank side). Unique
            per partner.

            Lets the bank locate the Safariat booking from its own primary key.
        traveler:
          $ref: '#/components/schemas/Traveler'
        rooms_allocation:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/RoomAllocation'
        special_requests:
          type: string
          maxLength: 1000
          description: Special requests (dietary requirements, accessibility, etc.).
        payment_confirmation:
          $ref: '#/components/schemas/PaymentConfirmation'
    Booking:
      allOf:
        - $ref: '#/components/schemas/BookingSummary'
        - type: object
          required:
            - traveler
            - rooms_allocation
            - payment_confirmation
            - snapshot_id
            - cancellation_tiers
          properties:
            traveler:
              $ref: '#/components/schemas/Traveler'
            rooms_allocation:
              type: array
              items:
                $ref: '#/components/schemas/RoomAllocation'
            special_requests:
              type: string
              nullable: true
            payment_confirmation:
              $ref: '#/components/schemas/PaymentConfirmationView'
            snapshot_id:
              type: string
              format: uuid
              description: >
                ID of the adventure snapshot captured at creation (see ADR-013).
                Guarantees

                the traveler receives the experience as it was at the time of
                booking.
            cancellation_tiers:
              type: array
              items:
                type: object
                properties:
                  days_before:
                    type: integer
                  refund_percent:
                    type: integer
                  refundable_amount:
                    $ref: '#/components/schemas/Money'
            voucher_url:
              type: string
              format: uri
              description: Pre-signed URL to the PDF voucher (7-day TTL).
    Error:
      type: object
      required:
        - code
        - message
        - request_id
      properties:
        code:
          type: string
          description: |
            Stable, documented i18n code. Form `domain.subdomain.detail`
            (e.g. `payment.amount.mismatch`, `auth.api_key.invalid`).
          example: validation.required.field
        message:
          type: string
          description: >-
            English message (for bank logs). API errors are never localized on
            the Safariat side.
          example: Field 'rate_pack_id' is required.
        request_id:
          type: string
          description: Unique request ID on the Safariat side.
        details:
          type: object
          description: Optional structured details (offending field, expected value, etc.).
          additionalProperties: true
    Traveler:
      type: object
      required:
        - first_name
        - last_name
        - email
        - phone
        - language
        - customer_reference
      properties:
        first_name:
          type: string
          minLength: 1
          maxLength: 100
        last_name:
          type: string
          minLength: 1
          maxLength: 100
        email:
          type: string
          format: email
        phone:
          type: string
          pattern: ^\+?[1-9]\d{6,14}$
          description: E.164 format recommended.
        language:
          type: string
          enum:
            - fr
            - en
            - ar
        customer_reference:
          type: string
          minLength: 1
          maxLength: 100
          description: >
            The bank's internal customer ID. **Pseudonymized server-side with
            keyed

            HMAC-SHA256, namespaced per partner** before storage
            (non-reversible,

            deterministic per partner). Lets the bank locate the traveler in its
            own

            systems, with no cross-bank correlation in the event of a Safariat
            breach.

            Also accepted as the `customer_reference` filter on `GET /bookings`.
    RoomAllocation:
      type: object
      required:
        - room_index
        - occupants
      properties:
        room_index:
          type: integer
          minimum: 0
          description: Room index (correlated with the position in `quote.rooms`).
        occupants:
          type: array
          minItems: 1
          items:
            type: object
            required:
              - type
            properties:
              type:
                type: string
                enum:
                  - ADULT
                  - CHILD
              age:
                type: integer
                minimum: 0
                maximum: 17
                description: Required if type=CHILD.
    PaymentConfirmation:
      type: object
      required:
        - bank_payment_ref
        - paid_amount_total
        - paid_amount_net
        - paid_at
        - payment_method
      description: >-
        Payment confirmation supplied by the bank on booking creation (request
        input).
      properties:
        bank_payment_ref:
          type: string
          minLength: 1
          maxLength: 255
          description: The bank's internal payment reference (for reconciliation).
        paid_amount_total:
          type: number
          format: double
          minimum: 0
          multipleOf: 0.01
          example: 1116
          description: >
            Total amount (in MAD) debited from the bank customer — a **plain
            decimal**, not a Money

            object. The bank may apply its own surcharge on top of the net
            price. Echoed back on the

            booking as `pricing.customer_price`.
        paid_amount_net:
          type: number
          format: double
          minimum: 0
          multipleOf: 0.01
          example: 949.9
          description: >
            **Net amount due to Safariat** (in MAD), a **plain decimal**. Must
            be strictly equal to

            `quote.pricing.amount_due_to_safariat.amount`, otherwise `422
            payment.amount.mismatch`.
        paid_at:
          type: string
          format: date-time
        payment_method:
          type: string
          enum:
            - CARD
            - BANK_TRANSFER
            - ACCOUNT_DEBIT
            - CASH
            - WAFACASH
            - OTHER
    BookingSummary:
      type: object
      required:
        - booking_number
        - partner_reference
        - status
        - adventure_slug
        - travel_date
        - pricing
        - created_at
      properties:
        booking_number:
          type: string
          example: MV-AB7X92
        partner_reference:
          type: string
        status:
          $ref: '#/components/schemas/BookingStatus'
        adventure_slug:
          type: string
        adventure_title:
          type: string
        travel_date:
          type: string
          format: date
        traveler_name:
          type: string
          description: Initialed (e.g. 'A. B.') for PII protection in lists.
        pricing:
          $ref: '#/components/schemas/PartnerPricing'
          description: >-
            Revenue breakdown with the real surcharge recorded at booking
            (`indicative = false`).
        settlement_id:
          type: string
          format: uuid
          nullable: true
          description: >-
            ID of the settlement this booking is included in (null if not yet
            invoiced).
        created_at:
          type: string
          format: date-time
    PaymentConfirmationView:
      type: object
      required:
        - bank_payment_ref
        - paid_at
        - payment_method
      description: >
        Payment confirmation echoed back on the booking (response projection).
        The monetary

        amounts (customer total, net due to Safariat, surcharge) are exposed via
        `pricing`.
      properties:
        bank_payment_ref:
          type: string
          description: The bank's internal payment reference (for reconciliation).
        paid_at:
          type: string
          format: date-time
        payment_method:
          type: string
          enum:
            - CARD
            - BANK_TRANSFER
            - ACCOUNT_DEBIT
            - CASH
            - WAFACASH
            - OTHER
    Money:
      type: object
      required:
        - amount
        - currency
      properties:
        amount:
          type: string
          pattern: ^-?\d+\.\d{2}$
          description: Amount as a decimal string with 2 decimals to preserve precision.
          example: '3200.00'
        currency:
          type: string
          enum:
            - MAD
          description: Currency — MAD only in V1.
          example: MAD
    BookingStatus:
      type: string
      enum:
        - CONFIRMED
        - COMPLETED
        - CANCELLED
      description: >
        The partner channel skips the `PENDING` status (no payment-wait window).

        Additional internal Safariat statuses (`PAYMENT_PROCESSING`,
        `REFUND_PENDING`)

        are never exposed on this API.
    PartnerPricing:
      type: object
      description: >
        Revenue breakdown for the AWB channel, in MAD. Canonical home for all
        partner-facing amounts.

        On catalog (search / adventure detail) and quote responses `indicative`
        is `true` and

        `sandbox_surcharge` is `0` (the bank sets its surcharge at booking
        time), so `customer_price`

        equals `b2c_price`. On booking responses `indicative` is `false` and the
        values reflect the

        amounts actually recorded.

        Invariant: `amount_due_to_safariat + awb_commission_share +
        sandbox_surcharge = customer_price`.
      required:
        - b2c_price
        - customer_price
        - amount_due_to_safariat
        - awb_commission_share
        - sandbox_surcharge
        - indicative
      properties:
        b2c_price:
          $ref: '#/components/schemas/Money'
          description: Public B2C reference price (price floor guaranteed to the customer).
        customer_price:
          $ref: '#/components/schemas/Money'
          description: >-
            Total price paid by the customer = `b2c_price + sandbox_surcharge`.
            Equals `b2c_price` before booking.
        amount_due_to_safariat:
          $ref: '#/components/schemas/Money'
          description: >-
            Net reversed to Safariat = operator share + 65% of the base
            commission (the daily wire amount).
        awb_commission_share:
          $ref: '#/components/schemas/Money'
          description: AWB's 35% share of the base commission.
        sandbox_surcharge:
          $ref: '#/components/schemas/Money'
          description: >-
            AWB's own surcharge (100% AWB), derived as `customer_price −
            b2c_price`. `0` before booking.
        indicative:
          type: boolean
          description: >
            `true` for catalog/quote "from" prices (final amounts confirmed at
            booking);

            `false` on booking responses.
  responses:
    BadRequest:
      description: Malformed request (syntactic validation).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missingField:
              value:
                code: validation.required.field
                message: Field 'rate_pack_id' is required.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
    Unauthorized:
      description: API key missing, invalid, expired, or incorrect HMAC signature.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              value:
                code: auth.api_key.missing
                message: Authorization header is required.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
            invalid:
              value:
                code: auth.api_key.invalid
                message: The provided API key is invalid or has been revoked.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
            signatureInvalid:
              value:
                code: auth.signature.invalid
                message: HMAC signature does not match.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
            timestampSkew:
              value:
                code: auth.timestamp.skew
                message: Timestamp is more than 5 minutes off server time.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
    TooManyRequests:
      description: Rate limit exceeded for the current key.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: rate_limit.exceeded
            message: Rate limit exceeded for this API key.
            request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
  headers:
    RetryAfter:
      schema:
        type: integer
      description: Seconds to wait before the next attempt (RFC 7231).
    XRateLimitLimit:
      schema:
        type: integer
      description: Request limit per minute for the current key.
    XRateLimitRemaining:
      schema:
        type: integer
      description: Requests remaining in the current window.
    XRateLimitReset:
      schema:
        type: integer
        format: int64
      description: Unix timestamp (s) at which the window resets.
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: sk_live_<prefix>_<secret> or sk_test_<prefix>_<secret>
      x-default: sk_test_DEMO0000_replace_with_your_sandbox_key
      description: >
        API key authentication. Issued by the Safariat admin or via the partner
        portal.

        The secret is shown only once at generation.


        Production `sk_live_*` keys must additionally sign every write request

        (`POST`/`PUT`/`DELETE`) with the `X-Timestamp` and `X-Signature`
        headers.

        Sandbox `sk_test_*` keys are exempt from request signing: `X-Signature`
        and

        `X-Timestamp` are not required for writes in the sandbox (so the
        developer-portal

        "Try it" playground works end to end). The `Idempotency-Key` header
        remains

        required on writes in both environments.

````