openapi: 3.1.0
info:
  title: Travelmode Partner Trips API
  version: 1.0.0
  summary: Start and sync travel-group trip plans from your marketplace.
  description: |
    The Partner Trips API lets a third-party marketplace's booking
    suppliers start trip plans in travelmode on behalf of their travel
    groups, hand each trip to its group organizer via a claim link, and
    keep the marketplace's copy of the trip description in sync — by
    pulling the partner trip view or by subscribing to signed `trip.*`
    webhooks.

    **Model.** Your organization is a travelmode developer account. You
    register each of your group providers as a **supplier** (attribution
    and reporting only). A trip you start is created ownerless, linked to
    your organization (and optionally a supplier), and carries a claim
    token; the group organizer opens the claim URL, signs in, and becomes
    the trip's owner in their own travelmode account. Your organization
    keeps API access to the trip through the link — before and after the
    claim.

    **Authentication.** Send your key as
    `Authorization: Bearer tm_trips_…`. Keys are issued per organization
    with `trips:*` scopes and are shown in plaintext exactly once.
  contact:
    name: Travelmode Developer Support
    url: https://travelmode.ai
  license:
    name: Proprietary
    url: https://travelmode.ai

servers:
  - url: https://api.travelmode.ai
    description: Production
  - url: https://{host}
    description: Self-hosted / Replit deployment. Replace `{host}` with your deployed domain; routes are served under `/v1/partner/...` on that host.
    variables:
      host:
        default: your-app-domain.replit.app

tags:
  - name: Organization
    description: Identify the calling organization.
  - name: Suppliers
    description: Register and manage your organization's group providers.
  - name: Trips
    description: Start, read, and update partner-linked trip plans.
  - name: Claim
    description: Hand a trip to its group organizer.
  - name: Webhooks
    description: Shared platform webhook endpoints; subscribe to `trip.*` events.

security:
  - bearerAuth: []

paths:
  /v1/partner/organization:
    get:
      tags: [Organization]
      operationId: getOrganization
      summary: Identify the calling organization (whoami)
      description: |
        Returns the stable `organization_id` that keys every supplier,
        trip, and webhook payload — use it to identify this organization
        in your own collection of organizations — plus the organization's
        name, status, plan, environment, and the key's scopes. Any valid
        partner key can call this; no specific scope is required.
      responses:
        "200":
          description: The calling organization.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/partner/suppliers:
    post:
      tags: [Suppliers]
      operationId: createSupplier
      summary: Register a supplier
      description: |
        Register one of your group providers. `external_id` is your own
        supplier id and is unique within your organization. Requires the
        `trips:suppliers` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [external_id, name]
              properties:
                external_id:
                  type: string
                  description: Your supplier id (unique per organization).
                name:
                  type: string
                metadata:
                  type: object
                  additionalProperties: true
                  description: Opaque partner metadata.
      responses:
        "201":
          description: Supplier created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupplierEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: A supplier with this `external_id` already exists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
    get:
      tags: [Suppliers]
      operationId: listSuppliers
      summary: List suppliers
      description: List your organization's suppliers. Requires `trips:suppliers`.
      responses:
        "200":
          description: Supplier list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupplierListEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/partner/suppliers/{supplierId}:
    parameters:
      - $ref: "#/components/parameters/SupplierId"
    get:
      tags: [Suppliers]
      operationId: getSupplier
      summary: Fetch a supplier
      responses:
        "200":
          description: Supplier.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupplierEnvelope"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [Suppliers]
      operationId: updateSupplier
      summary: Update a supplier
      description: Update name, status, or metadata. Requires `trips:suppliers`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                status:
                  type: string
                  enum: [active, disabled]
                metadata:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Updated supplier.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupplierEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Suppliers]
      operationId: disableSupplier
      summary: Disable a supplier
      description: |
        Soft-disable: the supplier stops being usable for new trips but
        its attribution on existing trips is preserved.
      responses:
        "200":
          description: Disabled supplier.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SupplierEnvelope"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/partner/trips:
    post:
      tags: [Trips]
      operationId: startTrip
      summary: Start trip planning
      description: |
        Create a trip shell linked to your organization. The response
        contains `claim.claim_url` — send it to the group organizer so
        they can take ownership — and `planner_url`, where planning
        continues in the travelmode UI after the claim. Requires
        `trips:write`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                description:
                  type: string
                  nullable: true
                summary:
                  type: string
                  nullable: true
                supplier_id:
                  type: string
                  description: Internal supplier id (from supplier registration).
                supplier_external_id:
                  type: string
                  description: Alternative — your own supplier id.
                external_ref:
                  type: string
                  nullable: true
                  description: Your group/booking reference for this trip.
                metadata:
                  type: object
                  additionalProperties: true
                  description: Free-form context (e.g. destination, dates).
      responses:
        "201":
          description: Trip created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerTripEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
    get:
      tags: [Trips]
      operationId: listPartnerTrips
      summary: List partner trips
      description: |
        List your organization's partner-linked trips. Filter with
        `supplier_id`, `external_ref`, or `claim_status`. Requires
        `trips:read`.
      parameters:
        - name: supplier_id
          in: query
          schema: { type: string }
        - name: external_ref
          in: query
          schema: { type: string }
        - name: claim_status
          in: query
          schema:
            type: string
            enum: [pending_claim, claimed, revoked]
      responses:
        "200":
          description: Trip list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerTripListEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/partner/trips/{tripId}:
    parameters:
      - $ref: "#/components/parameters/TripId"
    get:
      tags: [Trips]
      operationId: getPartnerTrip
      summary: Fetch the partner trip view (pull sync)
      description: |
        The partner view of a trip: name, description, summary, status,
        the public story URL once the organizer shares the trip, the
        revision counter, claim status, and your supplier/external
        references. Poll this — or subscribe to `trip.updated` webhooks —
        to mirror the trip description on your site. Requires
        `trips:read`. 404 unless the trip is linked to your organization.
      responses:
        "200":
          description: Partner trip view.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerTripEnvelope"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [Trips]
      operationId: updatePartnerTrip
      summary: Save/update a trip
      description: |
        Update `name`, `description`, or `summary` (trip fields — these
        fire `trip.updated` webhooks), and/or `external_ref` / `metadata`
        (link fields). Visibility and sharing remain owner-only.
        Requires `trips:write`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string, nullable: true }
                summary: { type: string, nullable: true }
                external_ref: { type: string, nullable: true }
                metadata:
                  type: object
                  additionalProperties: true
                  nullable: true
      responses:
        "200":
          description: Updated partner trip view.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerTripEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/partner/trips/{tripId}/claim-token:
    parameters:
      - $ref: "#/components/parameters/TripId"
    post:
      tags: [Claim]
      operationId: reissueClaimToken
      summary: Re-issue the claim token
      description: |
        Mint a fresh claim token (and URL) for a still-unclaimed trip,
        invalidating the previous one. Use when the original link
        expired or leaked. 409 once the trip has been claimed. Requires
        `trips:write`.
      responses:
        "200":
          description: Partner trip view with the new claim URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerTripEnvelope"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Trip already claimed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "`Authorization: Bearer tm_trips_…`"

  parameters:
    SupplierId:
      name: supplierId
      in: path
      required: true
      schema: { type: string }
    TripId:
      name: tripId
      in: path
      required: true
      schema: { type: integer, minimum: 1 }

  responses:
    BadRequest:
      description: Malformed request.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Unauthorized:
      description: Missing/invalid API key or missing scope.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    NotFound:
      description: Resource not found (or not linked to your organization).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"

  schemas:
    Meta:
      type: object
      properties:
        request_id: { type: string }
        environment: { type: string, enum: [test, production] }
        billing_units: { type: integer }

    ErrorEnvelope:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }
            details: {}
        meta:
          $ref: "#/components/schemas/Meta"

    Organization:
      type: object
      properties:
        organization_id:
          type: string
          description: Stable id of your organization; keys every resource.
        name: { type: string }
        status: { type: string, enum: [active, suspended, closed] }
        plan: { type: string }
        environment: { type: string, enum: [test, production] }
        scopes:
          type: array
          items: { type: string }
        created_at: { type: string, format: date-time }

    OrganizationEnvelope:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/Organization"
        meta:
          $ref: "#/components/schemas/Meta"

    Supplier:
      type: object
      properties:
        supplier_id: { type: string }
        organization_id: { type: string }
        external_id: { type: string }
        name: { type: string }
        status: { type: string, enum: [active, disabled] }
        metadata: { type: object, additionalProperties: true, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    SupplierEnvelope:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/Supplier"
        meta:
          $ref: "#/components/schemas/Meta"

    SupplierListEnvelope:
      type: object
      properties:
        data:
          type: object
          properties:
            suppliers:
              type: array
              items:
                $ref: "#/components/schemas/Supplier"
        meta:
          $ref: "#/components/schemas/Meta"

    PartnerTrip:
      type: object
      properties:
        trip_id: { type: integer }
        organization_id: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        summary: { type: string, nullable: true }
        status:
          type: string
          description: Trip lifecycle status (planning, upcoming, travel_mode, ...).
        visibility: { type: string, enum: [private, link, public] }
        share_slug: { type: string, nullable: true }
        public_url:
          type: string
          nullable: true
          description: Public story URL once the organizer shares the trip.
        planner_url: { type: string }
        revision:
          type: integer
          description: Monotonic change counter — dedupe webhook deliveries on it.
        updated_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        supplier_id: { type: string, nullable: true }
        external_ref: { type: string, nullable: true }
        claim:
          type: object
          properties:
            status: { type: string, enum: [pending_claim, claimed, revoked] }
            claim_url:
              type: string
              nullable: true
              description: Present while the trip is unclaimed.
            expires_at: { type: string, format: date-time, nullable: true }
            claimed_at: { type: string, format: date-time, nullable: true }
        metadata: { type: object, additionalProperties: true, nullable: true }

    PartnerTripEnvelope:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerTrip"
        meta:
          $ref: "#/components/schemas/Meta"

    PartnerTripListEnvelope:
      type: object
      properties:
        data:
          type: object
          properties:
            trips:
              type: array
              items:
                $ref: "#/components/schemas/PartnerTrip"
        meta:
          $ref: "#/components/schemas/Meta"

    TripWebhookEvent:
      type: object
      description: |
        Delivery body POSTed to your webhook endpoint for `trip.updated`
        and `trip.claimed` events. Register endpoints via the shared
        platform webhook API (`POST /v1/webhooks`, scope
        `trips:webhooks`) subscribing to the `trip.*` event types;
        deliveries are HMAC-SHA256 signed (`X-Travelmode-Signature`
        over `"<timestamp>.<body>"`). The trip payload is nested at
        `data.payload`.
      properties:
        id: { type: string, description: Event id. }
        type: { type: string, enum: [trip.updated, trip.claimed] }
        occurred_at: { type: string, format: date-time }
        data:
          type: object
          properties:
            event_id: { type: string }
            event_type: { type: string }
            severity: { type: string }
            payload:
              type: object
              properties:
                tripId: { type: integer }
                organizationId: { type: string }
                externalRef: { type: string, nullable: true }
                supplierId: { type: string, nullable: true }
                claimStatus: { type: string }
                changedFields:
                  type: array
                  items: { type: string }
                trip:
                  type: object
                  properties:
                    name: { type: string }
                    description: { type: string, nullable: true }
                    summary: { type: string, nullable: true }
                    status: { type: string }
                    revision: { type: integer }
                    updatedAt: { type: string, format: date-time, nullable: true }
