openapi: 3.1.0
info:
  title: Sanna Transfer - RowanX Booking Integration
  version: 0.2.0-proposal
  summary: RowanX sends transportation bookings to Sanna Transfer
  description: |
    RowanX sends signed booking create, update, and cancellation events to Sanna.
    Sanna sends final COMPLETED or NO_SHOW events to a callback supplied by RowanX.
    Environment activation and credentials are required before traffic is sent.
  contact:
    name: Sanna Transfer Integration Support
servers:
  - url: https://sanna-transfer.ha0-nguyen.workers.dev
    description: Staging
  - url: https://sanna-transfer.thecodeorigin.workers.dev
    description: Production
tags:
  - name: RowanX booking events
    description: Signed create, update, and cancellation events sent by RowanX to Sanna.
  - name: Sanna status callbacks
    description: Final booking status events sent by Sanna to RowanX.
paths:
  /webhook/partner/4:
    post:
      tags: [RowanX booking events]
      operationId: receiveRowanXBookingEvent
      summary: Create, update, or cancel a RowanX booking
      description: |
        Send events for one booking in version order. Retries keep the same exact
        body, eventId, and bookingVersion but use a fresh timestamp and signature.
      security:
        - inboundHmac: []
      parameters:
        - $ref: '#/components/parameters/KeyId'
        - $ref: '#/components/parameters/Timestamp'
        - $ref: '#/components/parameters/Signature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RowanXBookingEvent'
            examples:
              created:
                summary: Create a booking
                value:
                  eventId: evt_01J5V1Y8F8K3R4Z7Q2M6A9C0DE
                  eventType: booking.created
                  occurredAt: '2026-08-23T08:30:00Z'
                  bookingVersion: 1
                  booking:
                    id: ROWANX-48291
                    departureTime: '2026-09-05T08:30:00+07:00'
                    departure:
                      name: Da Nang International Airport
                      latitude: 16.0439
                      longitude: 108.1994
                    arrival:
                      name: Hoi An Ancient Town
                      latitude: 15.8801
                      longitude: 108.3380
                    passenger:
                      name: Nguyen Van A
                      phone: '+84901234567'
                      count: 3
                    vehicleCategory: sedan
                    flightNumber: VN123
              updated:
                summary: Replace the current booking snapshot
                value:
                  eventId: evt_01J5V2C6YX7D4Z1Q9B3N8F0KLM
                  eventType: booking.updated
                  occurredAt: '2026-08-23T09:15:00Z'
                  bookingVersion: 2
                  booking:
                    id: ROWANX-48291
                    departureTime: '2026-09-05T09:00:00+07:00'
                    departure:
                      name: Da Nang International Airport
                      latitude: 16.0439
                      longitude: 108.1994
                    arrival:
                      name: Hoi An Ancient Town
                      latitude: 15.8801
                      longitude: 108.3380
                    passenger:
                      name: Nguyen Van A
                      phone: '+84901234567'
                      count: 3
                    vehicleCategory: sedan
                    flightNumber: VN123
              cancelled:
                summary: Cancel before operation
                value:
                  eventId: evt_01J5V2Q7BD9S8E1TC4K6G0M3NP
                  eventType: booking.cancelled
                  occurredAt: '2026-08-23T10:00:00Z'
                  bookingVersion: 3
                  bookingId: ROWANX-48291
                  reason: Customer cancelled the booking
      responses:
        '200':
          description: Accepted or an identical duplicate
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '409':
          $ref: '#/components/responses/Error'
        '413':
          $ref: '#/components/responses/Error'
        '422':
          $ref: '#/components/responses/Error'
        '429':
          description: Rate limited; retry after the supplied delay
          headers:
            Retry-After:
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          $ref: '#/components/responses/Error'
        '503':
          $ref: '#/components/responses/Error'
webhooks:
  bookingStatusUpdated:
    post:
      tags: [Sanna status callbacks]
      operationId: receiveSannaBookingStatus
      summary: Receive a final booking status from Sanna
      description: |
        Sanna sends this payload to the environment-specific callback URL supplied
        by RowanX. RowanX should return a 2xx response after durable acceptance.
      security:
        - callbackHmac: []
      parameters:
        - $ref: '#/components/parameters/KeyId'
        - $ref: '#/components/parameters/Timestamp'
        - $ref: '#/components/parameters/Signature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BookingStatusUpdated'
            example:
              eventId: status_01J6A4YH5D9BX2K8P7C3N0Q1RM
              eventType: booking.status.updated
              occurredAt: '2026-09-05T03:15:00Z'
              bookingId: ROWANX-48291
              status: COMPLETED
      responses:
        '200':
          description: Status durably accepted
        '400':
          description: Invalid status callback
        '401':
          description: Invalid callback signature
        '500':
          description: Temporary RowanX failure; Sanna retries
components:
  securitySchemes:
    inboundHmac:
      type: apiKey
      in: header
      name: X-Sanna-Webhook-Signature
      description: HMAC-SHA256 signature for RowanX-to-Sanna events.
    callbackHmac:
      type: apiKey
      in: header
      name: X-Sanna-Webhook-Signature
      description: HMAC-SHA256 signature for Sanna-to-RowanX callbacks.
  parameters:
    KeyId:
      name: X-Sanna-Key-Id
      in: header
      required: true
      description: Environment-specific credential identifier; not the secret.
      schema:
        type: string
        minLength: 3
        maxLength: 64
    Timestamp:
      name: X-Sanna-Webhook-Timestamp
      in: header
      required: true
      description: Current Unix time in seconds. Maximum accepted skew is 300 seconds.
      schema:
        type: integer
        format: int64
    Signature:
      name: X-Sanna-Webhook-Signature
      in: header
      required: true
      description: |
        `v1=<base64(HMAC-SHA256(secret, keyId + "." + timestamp + "." + exactRawBody))>`
      schema:
        type: string
        pattern: '^v1=[A-Za-z0-9+/]{43}=$'
  schemas:
    RowanXBookingEvent:
      oneOf:
        - $ref: '#/components/schemas/BookingCreated'
        - $ref: '#/components/schemas/BookingUpdated'
        - $ref: '#/components/schemas/BookingCancelled'
      discriminator:
        propertyName: eventType
        mapping:
          booking.created: '#/components/schemas/BookingCreated'
          booking.updated: '#/components/schemas/BookingUpdated'
          booking.cancelled: '#/components/schemas/BookingCancelled'
    BookingCreated:
      type: object
      additionalProperties: false
      required: [eventId, eventType, occurredAt, bookingVersion, booking]
      properties:
        eventId:
          $ref: '#/components/schemas/EventId'
        eventType:
          type: string
          const: booking.created
        occurredAt:
          $ref: '#/components/schemas/TimestampValue'
        bookingVersion:
          type: integer
          const: 1
        booking:
          $ref: '#/components/schemas/Booking'
    BookingUpdated:
      type: object
      additionalProperties: false
      required: [eventId, eventType, occurredAt, bookingVersion, booking]
      properties:
        eventId:
          $ref: '#/components/schemas/EventId'
        eventType:
          type: string
          const: booking.updated
        occurredAt:
          $ref: '#/components/schemas/TimestampValue'
        bookingVersion:
          type: integer
          minimum: 2
        booking:
          $ref: '#/components/schemas/Booking'
    BookingCancelled:
      type: object
      additionalProperties: false
      required: [eventId, eventType, occurredAt, bookingVersion, bookingId, reason]
      properties:
        eventId:
          $ref: '#/components/schemas/EventId'
        eventType:
          type: string
          const: booking.cancelled
        occurredAt:
          $ref: '#/components/schemas/TimestampValue'
        bookingVersion:
          type: integer
          minimum: 2
        bookingId:
          $ref: '#/components/schemas/BookingId'
        reason:
          type: string
          minLength: 1
          maxLength: 500
    Booking:
      type: object
      additionalProperties: false
      description: Complete current booking snapshot for create or update.
      required: [id, departureTime, departure, arrival, passenger]
      properties:
        id:
          $ref: '#/components/schemas/BookingId'
        departureTime:
          $ref: '#/components/schemas/TimestampValue'
        departure:
          $ref: '#/components/schemas/Location'
        arrival:
          $ref: '#/components/schemas/Location'
        passenger:
          $ref: '#/components/schemas/Passenger'
        vehicleCategory:
          type: [string, 'null']
          maxLength: 100
        flightNumber:
          type: [string, 'null']
          maxLength: 30
        notes:
          type: [string, 'null']
          maxLength: 500
          description: Operational notes only; no payment data, credentials, or secrets.
    Location:
      type: object
      additionalProperties: false
      required: [name, latitude, longitude]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 300
        latitude:
          type: number
          minimum: -90
          maximum: 90
        longitude:
          type: number
          minimum: -180
          maximum: 180
    Passenger:
      type: object
      additionalProperties: false
      required: [name, phone, count]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        phone:
          type: string
          minLength: 7
          maxLength: 20
        count:
          type: integer
          minimum: 1
          maximum: 99
    BookingStatusUpdated:
      type: object
      additionalProperties: false
      required: [eventId, eventType, occurredAt, bookingId, status]
      properties:
        eventId:
          $ref: '#/components/schemas/EventId'
        eventType:
          type: string
          const: booking.status.updated
        occurredAt:
          $ref: '#/components/schemas/TimestampValue'
        bookingId:
          $ref: '#/components/schemas/BookingId'
        status:
          type: string
          enum: [COMPLETED, NO_SHOW]
        reason:
          type: [string, 'null']
          maxLength: 500
    EventId:
      type: string
      minLength: 8
      maxLength: 80
      description: Unique within one environment and stable across retries.
    BookingId:
      type: string
      minLength: 1
      maxLength: 100
      pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]*$'
    TimestampValue:
      type: string
      format: date-time
      description: RFC 3339 timestamp containing Z or an explicit UTC offset.
    AcceptedResponse:
      type: object
      additionalProperties: false
      required: [status, eventId, bookingId]
      properties:
        status:
          type: string
          enum: [accepted, duplicate]
        eventId:
          $ref: '#/components/schemas/EventId'
        bookingId:
          $ref: '#/components/schemas/BookingId'
    ErrorResponse:
      type: object
      additionalProperties: false
      required: [error, message, retryable]
      properties:
        error:
          type: string
          enum:
            - invalid_request
            - payload_too_large
            - invalid_signature
            - stale_timestamp
            - schema_invalid
            - event_id_conflict
            - stale_version
            - version_gap
            - lifecycle_conflict
            - rate_limited
            - temporary_failure
        message:
          type: string
        retryable:
          type: boolean
  responses:
    Error:
      description: Request rejected; inspect the stable error code and retryable flag.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
