openapi: 3.0.3

info:
  title: Taxi Al-Mumayaz Partner API
  version: "v1"
  description: |
    The Taxi Al-Mumayaz Partner API is a REST/JSON API that lets approved B2B
    partners look up service coverage and pricing, read their company account,
    and create airport-taxi bookings on behalf of their end users.

    Bookings are billed against your company's prepaid **credit balance** held with
    Taxi Al-Mumayaz. **Prices are always computed server-side** — your application
    never sends prices.

    **Sandbox first.** All credentials, pricing, and balances in the sandbox
    environment are for testing. Validate your full integration against the sandbox
    base URL before requesting production credentials.

    ## Authentication model
    Exchange long-lived **client credentials** (`client_id` / `client_secret`) for a
    short-lived **bearer token** at `POST /v1/auth/token`, then send
    `Authorization: Bearer <token>` on every other endpoint. Tokens are valid for
    **1 hour**; there is no refresh endpoint — request a new token when one expires
    or when you receive a `401`.

    ## Conventions
    - **Transport:** HTTPS only. All requests and responses are JSON (UTF-8).
    - **Currency:** all monetary amounts are in **Iraqi Dinar (IQD)** as
      whole-number integers.
    - **Times:** ISO-8601. For local times use the Baghdad offset `+03:00`
      (e.g. `2026-06-22T14:30:00+03:00`). A timestamp without an offset is
      interpreted as UTC and shifts the time by 3 hours.
    - **Company scoping:** all data is scoped to the company linked to your partner
      app. You can never read or write another company's data.
    - **Request ID:** send `X-Request-Id` to have it echoed back as `x-request-id`;
      if omitted, the API generates one. Include it when reporting issues.
    - **Max body size:** request bodies are capped at **64 KB** (`413` otherwise).
  contact:
    name: Taxi Al-Mumayaz Partner Support
    # CONTACT — to be added (email / portal). Placeholder kept in sync with PARTNER_API.md §14.

servers:
  - url: https://api-sandbox.taxialmumayaz.net/functions/v1/partner-api
    description: Sandbox — use for all testing and integration validation.
  - url: https://{productionHost}/functions/v1/partner-api
    description: >-
      Production — your production host and credentials are issued during
      onboarding. Contact Taxi Al-Mumayaz to obtain them.
    variables:
      productionHost:
        default: production-host.taxialmumayaz.net
        description: Issued to you during onboarding.

tags:
  - name: Authentication
    description: Exchange client credentials for a short-lived bearer token.
  - name: Catalog & pricing
    description: Service zones, vehicle types, add-on services, and areas.
  - name: Account
    description: Your linked company and its credit balance.
  - name: Bookings
    description: Create and read bookings billed to your company credit.

security:
  - bearerAuth: []

paths:

  /v1/auth/token:
    post:
      tags: [Authentication]
      summary: Get an access token
      operationId: createToken
      description: |
        Exchange your `client_id` / `client_secret` for a short-lived bearer token.
        **Public** — no token required. The returned token is bound to your
        `partner_app_id` and `company_id` and is valid for 1 hour.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenRequest'
            example:
              client_id: "pk_live_xxxxxxxx"
              client_secret: "sk_live_xxxxxxxx"
      responses:
        '200':
          description: Token issued.
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              example:
                access_token: "<token>"
                token_type: "Bearer"
                expires_in: 3600
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Wrong `client_id` / `client_secret`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: invalid_credentials
                  message: Invalid client_id or client_secret
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/zones:
    get:
      tags: [Catalog & pricing]
      summary: List zones and the pricing matrix
      operationId: listZones
      description: |
        Active service zones plus the full pricing matrix. Catalog responses change
        rarely — cache them for 5–10 minutes. Use the pricing matrix to preview a
        booking total before creating it.
      parameters:
        - $ref: '#/components/parameters/XRequestIdParam'
      responses:
        '200':
          description: Zones and pricing rows.
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZonesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/vehicle-types:
    get:
      tags: [Catalog & pricing]
      summary: List vehicle types
      operationId: listVehicleTypes
      parameters:
        - $ref: '#/components/parameters/XRequestIdParam'
      responses:
        '200':
          description: Available vehicle types.
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VehicleTypesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/services:
    get:
      tags: [Catalog & pricing]
      summary: List add-on services
      operationId: listServices
      description: |
        Optional add-on services. **Service prices are charged per passenger** —
        see the pricing model in the API description.
      parameters:
        - $ref: '#/components/parameters/XRequestIdParam'
      responses:
        '200':
          description: Available add-on services.
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ServicesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/areas:
    get:
      tags: [Catalog & pricing]
      summary: List areas
      operationId: listAreas
      description: Areas, optionally filtered by zone via the `zone_id` query parameter.
      parameters:
        - name: zone_id
          in: query
          required: false
          description: Restrict results to a single zone.
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/XRequestIdParam'
      responses:
        '200':
          description: Areas (optionally filtered by zone).
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AreasResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/zones/{id}/areas:
    get:
      tags: [Catalog & pricing]
      summary: List areas for one zone
      operationId: listZoneAreas
      description: Areas scoped to a single zone via the path.
      parameters:
        - name: id
          in: path
          required: true
          description: Zone ID.
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/XRequestIdParam'
      responses:
        '200':
          description: Areas for the given zone.
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AreasResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/account:
    get:
      tags: [Account]
      summary: Get your company account
      operationId: getAccount
      description: |
        Your linked company, including its current credit balance (the same balance
        shown in your Taxi Al-Mumayaz partner account portal). `credit_balance` is in IQD.
      parameters:
        - $ref: '#/components/parameters/XRequestIdParam'
      responses:
        '200':
          description: Your company account.
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /v1/bookings:
    get:
      tags: [Bookings]
      summary: List your bookings
      operationId: listBookings
      description: Your bookings, newest first.
      parameters:
        - name: limit
          in: query
          required: false
          description: Page size. Default 50, max 100.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of records to skip. Default 0.
          schema:
            type: integer
            minimum: 0
            default: 0
        - $ref: '#/components/parameters/XRequestIdParam'
      responses:
        '200':
          description: A page of bookings.
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookingsListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      tags: [Bookings]
      summary: Create a booking
      operationId: createBooking
      description: |
        Creates a booking. The price is computed **server-side**; any price fields
        you send are ignored. The total is **atomically** deducted from your company
        credit balance in the same transaction that creates the booking — so credit
        is never debited without a booking, and vice-versa. Partner bookings are
        always billed to credit (you do not send a payment method).

        **`Idempotency-Key` is required** so a network retry never creates two
        bookings. Reuse the **same** key (8–128 chars) when retrying the **same**
        booking: same key + same body returns the original booking; same key +
        different body returns `409 idempotency_conflict`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/XRequestIdParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBookingRequest'
            example:
              passenger_name: "Ahmed Ali"
              passenger_phone: "+9647701234567"
              vehicle_type_id: "00000000-0000-0000-0000-000000000000"
              trip_type: "arrival"
              to_zone_id: "00000000-0000-0000-0000-000000000000"
              passengers_count: 2
      responses:
        '201':
          description: Booking created.
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookingResponse'
              example:
                booking:
                  id: "00000000-0000-0000-0000-000000000000"
                  status: "requested"
                  trip_type: "arrival"
                  from_zone_id: null
                  to_zone_id: "00000000-0000-0000-0000-000000000000"
                  from_area_id: null
                  to_area_id: null
                  vehicle_type_id: "00000000-0000-0000-0000-000000000000"
                  service_ids: []
                  passenger_name: "Ahmed Ali"
                  passenger_phone: "+9647701234567"
                  passengers_count: 2
                  luggage_count: 0
                  airline_name: null
                  arrival_time: null
                  notes: null
                  creator_name: null
                  vehicle_price: 30000
                  services_price: 0
                  discount_amount: 0
                  total_price: 30000
                  currency: "IQD"
                  payment_method: "credit"
                  payment_status: "paid"
                  company_id: "00000000-0000-0000-0000-000000000000"
                  partner_app_id: "00000000-0000-0000-0000-000000000000"
                  created_at: "2026-06-21T20:42:11+03:00"
                  updated_at: "2026-06-21T20:42:11+03:00"
        '400':
          description: |
            Malformed JSON, missing/invalid fields, missing/invalid `Idempotency-Key`,
            unknown `trip_type` (only `arrival`/`departure`), `service_ids` that do not
            exist or are inactive (`details.service_ids`), `invalid_arrival_time`, or
            `Pricing failed: no_price_for_route`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                unknown_service_ids:
                  value:
                    error:
                      code: bad_request
                      message: Unknown or inactive service_ids
                      details:
                        service_ids: ["00000000-0000-0000-0000-000000000000"]
                no_price_for_route:
                  value:
                    error:
                      code: bad_request
                      message: "Pricing failed: no_price_for_route"
                invalid_arrival_time:
                  value:
                    error:
                      code: invalid_request
                      message: invalid_arrival_time
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientBalance'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: |
            `idempotency_conflict` (same key, different body) or
            `idempotency_in_progress` (a request with this key is still processing).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                conflict:
                  value:
                    error:
                      code: idempotency_conflict
                      message: Idempotency-Key reused with a different request body
                in_progress:
                  value:
                    error:
                      code: idempotency_in_progress
                      message: A request with this Idempotency-Key is still being processed
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      callbacks:
        bookingEvent:
          '{your registered webhook URL}':
            post:
              summary: Webhook event (Taxi Al-Mumayaz → your endpoint)
              operationId: webhookBookingEvent
              description: |
                If you register a webhook URL, Taxi Al-Mumayaz POSTs a **signed** JSON
                event to it whenever something happens to one of your bookings. This
                describes the request **your** HTTPS endpoint must accept.

                **Requirements:** reachable over HTTPS on port 443 (publicly resolvable —
                no private/loopback addresses), accept `POST` with `application/json`, and
                respond **HTTP 200 within 5 seconds**. Any non-2xx, 3xx redirect, or
                timeout is a delivery failure (retried up to 8 attempts over ~2–3 days,
                then `dead`).

                **Verify the signature** before trusting the payload:
                `v1 = HMAC-SHA256(webhook_secret, "<t>.<raw-request-body>")` in hex, where
                `<t>` is the unix timestamp from `X-Premium-Signature`. Use the **raw
                bytes** received — re-stringifying parsed JSON produces a different
                signature. Reject if `|now − t| > 300` seconds. Deduplicate on
                `X-Premium-Event-Id`.

                **Event catalog:** `ping` (admin test), `booking.created`,
                `booking.status_changed`, `booking.payment_updated`.
              parameters:
                - name: User-Agent
                  in: header
                  schema:
                    type: string
                    example: PremiumTaxi-Webhook/1.0
                - name: X-Premium-Event
                  in: header
                  description: Event type.
                  schema:
                    type: string
                    example: booking.status_changed
                - name: X-Premium-Event-Id
                  in: header
                  description: Unique event ID (deduplicate on this).
                  schema:
                    type: string
                    format: uuid
                - name: X-Premium-Signature
                  in: header
                  description: "t=<unix>,v1=<hex-hmac-sha256>"
                  schema:
                    type: string
                    example: t=1782070177,v1=<hex-hmac-sha256>
              requestBody:
                required: true
                content:
                  application/json:
                    schema:
                      $ref: '#/components/schemas/WebhookEvent'
                    example:
                      id: "00000000-0000-0000-0000-000000000000"
                      type: booking.status_changed
                      created_at: "2026-06-21T20:45:00+03:00"
                      data:
                        id: "00000000-0000-0000-0000-000000000000"
                        status: confirmed
              responses:
                '200':
                  description: Return 200 within 5 seconds to acknowledge receipt.

  /v1/bookings/{id}:
    get:
      tags: [Bookings]
      summary: Get a booking by ID
      operationId: getBooking
      description: A single booking by ID, scoped to your partner app. Returns `404` if it isn't yours.
      parameters:
        - name: id
          in: path
          required: true
          description: Booking ID.
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/XRequestIdParam'
      responses:
        '200':
          description: The booking.
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookingResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Short-lived access token from `POST /v1/auth/token`. Valid for 1 hour.
        Send as `Authorization: Bearer <token>` on every endpoint except the token
        endpoint itself.

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Unique string (8–128 chars) per booking attempt; reuse on retry.
      schema:
        type: string
        minLength: 8
        maxLength: 128
    XRequestIdParam:
      name: X-Request-Id
      in: header
      required: false
      description: Optional client-supplied request ID; echoed back as `x-request-id`.
      schema:
        type: string

  headers:
    XRequestId:
      description: Request correlation ID. Include it when contacting support.
      schema:
        type: string

  schemas:

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Machine-readable error code.
            message:
              type: string
              description: Human-readable explanation.
            details:
              type: object
              additionalProperties: true
              description: Optional; present only for some errors (e.g. field lists, required balance).

    TokenRequest:
      type: object
      required: [client_id, client_secret]
      properties:
        client_id:
          type: string
          maxLength: 128
        client_secret:
          type: string
          maxLength: 256

    TokenResponse:
      type: object
      properties:
        access_token:
          type: string
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          description: Token lifetime in seconds (3600 = 1 hour).
          example: 3600

    Zone:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name_en:
          type: string
        name_ar:
          type: string
        is_active:
          type: boolean

    PricingRow:
      type: object
      properties:
        zone_id:
          type: string
          format: uuid
        vehicle_type_id:
          type: string
          format: uuid
        direction:
          type: string
          enum: [airport_to_zone, zone_to_airport, both]
        fixed_price:
          type: integer
          description: Per-trip fixed price in IQD.
        currency:
          type: string
          example: IQD

    ZonesResponse:
      type: object
      properties:
        zones:
          type: array
          items:
            $ref: '#/components/schemas/Zone'
        pricing:
          type: array
          items:
            $ref: '#/components/schemas/PricingRow'

    VehicleType:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name_en:
          type: string
        name_ar:
          type: string
        capacity:
          type: integer
          description: Maximum passengers.
        luggage_capacity:
          type: integer
          description: Maximum luggage pieces.
        image_url:
          type: string
          format: uri
          nullable: true
          description: Public HTTPS image (may be null); embeddable directly, no auth.
        is_active:
          type: boolean

    VehicleTypesResponse:
      type: object
      properties:
        vehicle_types:
          type: array
          items:
            $ref: '#/components/schemas/VehicleType'

    Service:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name_en:
          type: string
        name_ar:
          type: string
        price:
          type: integer
          description: Per-passenger price in IQD.
        is_active:
          type: boolean

    ServicesResponse:
      type: object
      properties:
        services:
          type: array
          items:
            $ref: '#/components/schemas/Service'

    Area:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name_en:
          type: string
        name_ar:
          type: string
        zone_id:
          type: string
          format: uuid
        is_active:
          type: boolean

    AreasResponse:
      type: object
      properties:
        areas:
          type: array
          items:
            $ref: '#/components/schemas/Area'

    Company:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
        credit_balance:
          type: integer
          description: Current prepaid credit balance in IQD.
        is_active:
          type: boolean

    AccountResponse:
      type: object
      properties:
        company:
          $ref: '#/components/schemas/Company'

    Booking:
      type: object
      description: Partner-safe booking object. Internal columns are never exposed; ignore any field you don't recognize.
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          description: >-
            Trip stage. Normal flow: requested → confirmed → active → completed,
            or cancelled at any point. Returned lowercase; treat as a free string
            (new states may be added), compare case-insensitively.
          example: requested
        trip_type:
          type: string
          enum: [arrival, departure]
        from_zone_id:
          type: string
          format: uuid
          nullable: true
        to_zone_id:
          type: string
          format: uuid
          nullable: true
        from_area_id:
          type: string
          format: uuid
          nullable: true
        to_area_id:
          type: string
          format: uuid
          nullable: true
        vehicle_type_id:
          type: string
          format: uuid
        service_ids:
          type: array
          items:
            type: string
            format: uuid
        passenger_name:
          type: string
        passenger_phone:
          type: string
        passengers_count:
          type: integer
        luggage_count:
          type: integer
        airline_name:
          type: string
          nullable: true
        arrival_time:
          type: string
          format: date-time
          nullable: true
        notes:
          type: string
          nullable: true
        creator_name:
          type: string
          nullable: true
        vehicle_price:
          type: integer
          description: Per-trip vehicle price in IQD.
        services_price:
          type: integer
          description: Sum of selected service prices × passengers_count, in IQD.
        discount_amount:
          type: integer
          description: Always 0 for partner bookings.
        total_price:
          type: integer
          description: vehicle_price + services_price, in IQD.
        currency:
          type: string
          example: IQD
        payment_method:
          type: string
          description: Always "credit" for partner bookings.
          example: credit
        payment_status:
          type: string
          enum: [paid, pending, unpaid]
        company_id:
          type: string
          format: uuid
        partner_app_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    BookingResponse:
      type: object
      properties:
        booking:
          $ref: '#/components/schemas/Booking'

    BookingsListResponse:
      type: object
      properties:
        bookings:
          type: array
          items:
            $ref: '#/components/schemas/Booking'
        limit:
          type: integer
        offset:
          type: integer

    WebhookEvent:
      type: object
      description: Signed event delivered to your registered webhook endpoint.
      properties:
        id:
          type: string
          format: uuid
          description: The event's unique ID (same as X-Premium-Event-Id).
        type:
          type: string
          enum: [ping, booking.created, booking.status_changed, booking.payment_updated]
        created_at:
          type: string
          format: date-time
        data:
          type: object
          description: >-
            For booking events, the partner-safe booking object (same fields as
            the Booking schema). For ping, a small test object. Internal columns
            are never included.
          additionalProperties: true

    CreateBookingRequest:
      type: object
      required: [passenger_name, passenger_phone, vehicle_type_id]
      description: |
        Send the zone for the **city-side** end only — the airport (Baghdad
        International) is always the other end and is fixed.
        For `arrival` (airport → zone) send `to_zone_id`; for `departure`
        (zone → airport) send `from_zone_id`.
        **Do not send** price, total_price, vehicle_price, services_price,
        discount_amount, payment_method, or status — they are computed/owned by
        Taxi Al-Mumayaz and any value you send is ignored.
      properties:
        passenger_name:
          type: string
          minLength: 1
          maxLength: 100
        passenger_phone:
          type: string
          description: 7–15 digits.
        vehicle_type_id:
          type: string
          format: uuid
          description: From GET /v1/vehicle-types.
        trip_type:
          type: string
          enum: [arrival, departure]
          default: arrival
          description: Only `arrival` or `departure`; any other value is rejected with 400.
        to_zone_id:
          type: string
          format: uuid
          description: Destination zone. Required when trip_type = arrival.
        from_zone_id:
          type: string
          format: uuid
          description: Origin zone. Required when trip_type = departure.
        from_area_id:
          type: string
          format: uuid
        to_area_id:
          type: string
          format: uuid
        service_ids:
          type: array
          items:
            type: string
            format: uuid
          description: From GET /v1/services; each must exist and be active, else 400.
        passengers_count:
          type: integer
          minimum: 1
          maximum: 50
          default: 1
          description: Values outside 1–50 are clamped.
        luggage_count:
          type: integer
          minimum: 0
          maximum: 50
          default: 0
          description: Values outside 0–50 are clamped; does not affect price.
        arrival_time:
          type: string
          format: date-time
          description: >-
            ISO-8601 with the `+03:00` offset, e.g. 2026-06-22T14:30:00+03:00.
            For arrival = flight arrival time at the airport; for departure =
            pickup time at the zone. Not validated as future/past — your
            integration is responsible for sending a correct, intended time.
        airline_name:
          type: string
          maxLength: 100
        notes:
          type: string
          maxLength: 500
        creator_name:
          type: string
          maxLength: 100
          description: The agent/user in your app who created the booking.

  responses:

    BadRequest:
      description: Malformed JSON, missing/invalid fields, or bad Idempotency-Key. `details.fields` lists offending fields when relevant.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: bad_request
              message: Missing or invalid fields
              details:
                fields: ["passenger_name"]

    Unauthorized:
      description: Missing, expired, or invalid bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unauthorized
              message: Missing or invalid access token

    InsufficientBalance:
      description: Company credit balance is below the booking total. `details.required` is the amount needed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: insufficient_balance
              message: Insufficient credit balance
              details:
                required: 30000

    Forbidden:
      description: Company inactive, or source IP not in `allowed_ips`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: forbidden
              message: Company inactive or IP not allowed

    NotFound:
      description: Resource not found, or unknown endpoint.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: not_found
              message: Resource not found

    PayloadTooLarge:
      description: Request body exceeds 64 KB.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: payload_too_large
              message: Request body exceeds 64 KB

    RateLimited:
      description: Per-minute (or auth) rate limit exceeded. Back off and retry after a short delay.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: rate_limited
              message: Too many requests

    ServerError:
      description: Unexpected server error. Safe to retry idempotent requests with the same Idempotency-Key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: server_error
              message: Unexpected server error

