openapi: 3.1.0
info:
  title: TDS relay mailbox
  version: 0.10.0
  summary: HTTPS mailbox for TDS CSV profile files, for providers that can only call out.
  description: 'The relay mailbox (SPEC.md §9.4) moves CSV profile files (SPEC.md §9.2) between a requester
    and a provider whose scheduler cannot accept connections: behind an agency firewall, or inside a platform
    such as Google Apps Script. Unlike spec/openapi.yaml, it is served by the **requester** (broker or
    coordinator tool), and the provider''s agent is the client. The agent lists the request files waiting,
    downloads each one, acknowledges it once it is safely stored, and uploads answers and statuses files.
    Every call is safe to repeat: acknowledgements are idempotent and uploads are write-once by name,
    so no Idempotency-Key is used. The credential names one provider''s mailbox; the provider is never
    taken from the request.'
  contact:
    name: tds-openapi maintainers
    url: https://github.com/kovirlabs/tds-openapi
  license:
    name: Apache-2.0
    identifier: Apache-2.0
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
servers:
- url: https://broker.example.org/api/relay
  description: A requester's mailbox base URL. Each requester publishes its own; agents are configured
    with the whole base URL, not a host name.
- url: http://localhost:4010/mailbox
  description: Reference mailbox in tds-mock (uv run tds-mock); the token is the provider id
security:
- mailboxToken: []
tags:
- name: Mailbox
  description: Relay mailbox for CSV profile files (SPEC.md §9.4).
paths:
  /outbox:
    get:
      operationId: listMailboxFiles
      summary: List request files waiting
      description: Request files the agent has not acknowledged yet, oldest first, with each file's size
        and SHA-256, the provider whose mailbox this is, and the poll interval the requester expects (SPEC.md
        §9.4). Every authorised call counts as a poll; an agent that has a file already (same name and
        SHA-256) acknowledges it again instead of downloading it.
      tags:
      - Mailbox
      parameters:
      - $ref: '#/components/parameters/RelayAgent'
      responses:
        '200':
          description: The waiting files (possibly none).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/mailbox-listing'
              examples:
                one_waiting:
                  value:
                    provider_id: county-dial-a-ride
                    poll_interval_s: 300
                    max_file_bytes: 10485760
                    files:
                    - name: tds_requests_20261104T150000Z.csv
                      size: 1198
                      sha256: 57dd4befc5ddd8a97df0ab5cff0fd86282dccf3a5e3990db4d804cff57e3b673
                      created_at: '2026-11-04T15:00:00Z'
                none_waiting:
                  value:
                    provider_id: county-dial-a-ride
                    poll_interval_s: 300
                    max_file_bytes: 10485760
                    files: []
                with_upload_reports:
                  value:
                    provider_id: county-dial-a-ride
                    requester_id: throughride-demo
                    poll_interval_s: 300
                    upload_reports: true
                    max_file_bytes: 10485760
                    files: []
        '401':
          description: Missing or invalid mailbox token (code UNAUTHORIZED). A requester MAY answer 403
            with the same code for a valid credential that is not a mailbox token.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/error'
              examples:
                no_token:
                  value:
                    type: about:blank
                    title: Unauthorized
                    status: 401
                    detail: 'Send Authorization: Bearer <mailbox token>.'
                    code: UNAUTHORIZED
        '429':
          $ref: '#/components/responses/MailboxTooManyRequests'
        '503':
          description: 'Back-pressure (SPEC.md §7.2): the mailbox cannot take calls for now (code TEMPORARILY_UNAVAILABLE).
            Poll again after the wait in Retry-After / retry_after_s.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/error'
              examples:
                maintenance:
                  value:
                    type: about:blank
                    title: Temporarily unavailable
                    status: 503
                    detail: The mailbox is down for maintenance.
                    code: TEMPORARILY_UNAVAILABLE
                    retry_after_s: 120
  /outbox/{file_name}:
    parameters:
    - $ref: '#/components/parameters/MailboxFileName'
    get:
      operationId: getMailboxFile
      summary: Download a request file
      description: One request file's bytes (SPEC.md §9.4). The agent checks X-Relay-SHA256 (and the listing's
        sha256) against the bytes before saving the file, and acknowledges it only after it is safely
        stored. A file stays downloadable after its acknowledgement until the requester deletes it under
        its retention rules. Reading a file changes nothing.
      tags:
      - Mailbox
      parameters:
      - $ref: '#/components/parameters/RelayAgent'
      responses:
        '200':
          description: The file, unchanged, as UTF-8 CSV (SPEC.md §9.2).
          headers:
            X-Relay-SHA256:
              $ref: '#/components/headers/RelaySHA256'
          content:
            text/csv:
              schema:
                type: string
              examples:
                requests_file:
                  value: 'requester_id,provider_id,trip_id,leg_id,action,revision,rider_ref_id,rider_display_name,rider_language,rider_contact_consent,rider_phone,attendants,pickup_lat,pickup_lon,pickup_name,pickup_street,pickup_city,pickup_region,pickup_postal_code,pickup_country,dropoff_lat,dropoff_lon,dropoff_name,pickup_earliest,pickup_latest,dropoff_earliest,dropoff_latest,service_requirements,notes_to_driver,funding_source,authorization_ref

                    throughride-demo,county-dial-a-ride,tr-20261105-0001,leg-1,REQUEST,1,r-91c2,Ana,es,true,+15175550142,1,42.2601,-83.7795,Home,210
                    Example St,Ann Arbor,MI,48103,US,42.2836,-83.7286,"Regional Hospital, east entrance",2026-11-05T08:00:00-05:00,2026-11-05T08:30:00-05:00,,,WHEELCHAIR_LIFT|DOOR_TO_DOOR,"Ring
                    bell twice, ramp at side door",MEDICAID_NEMT,PA-55120

                    throughride-demo,county-dial-a-ride,tr-20261105-0002,leg-1,REQUEST,1,r-4be0,,,,,,42.2215,-83.687,,,,,,,42.2475,-83.7426,Senior
                    Center,,,2026-11-05T09:30:00-05:00,2026-11-05T10:00:00-05:00,,,SECTION_5311,

                    throughride-demo,county-dial-a-ride,tr-20261105-0003,leg-1,REQUEST,1,r-77d1,,,,,,42.2601,-83.7795,,,,,,,42.9634,-85.6681,Out-of-area
                    destination,2026-11-05T07:00:00-05:00,2026-11-05T07:30:00-05:00,,,,,RIDER,

                    '
        '401':
          $ref: '#/components/responses/MailboxUnauthorized'
        '404':
          description: No request file with this name in this mailbox, including one already deleted (code
            NOT_FOUND).
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/error'
              examples:
                unknown_file:
                  value:
                    type: about:blank
                    title: Not found
                    status: 404
                    detail: No file tds_requests_20261103T090000Z.csv in this mailbox.
                    code: NOT_FOUND
        '429':
          $ref: '#/components/responses/MailboxTooManyRequests'
        '503':
          $ref: '#/components/responses/MailboxUnavailable'
  /ack/{file_name}:
    parameters:
    - $ref: '#/components/parameters/MailboxFileName'
    post:
      operationId: ackMailboxFile
      summary: Acknowledge a request file
      description: 'The agent has the file safely stored; it leaves the listing (SPEC.md §9.4). Idempotent:
        acknowledging again returns 200 with the first acknowledgement''s time. An agent that crashes
        between download and acknowledgement sees the file listed again and acknowledges it then. No request
        body and no Idempotency-Key.'
      tags:
      - Mailbox
      parameters:
      - $ref: '#/components/parameters/RelayAgent'
      responses:
        '200':
          description: The file is acknowledged.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/mailbox-ack'
              examples:
                acked:
                  value:
                    name: tds_requests_20261104T150000Z.csv
                    acked_at: '2026-11-04T15:04:10Z'
        '401':
          $ref: '#/components/responses/MailboxUnauthorized'
        '404':
          description: No request file with this name in this mailbox (code NOT_FOUND), for example one
            already deleted under the requester's retention rules. An agent that holds the file keeps
            it.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/error'
        '429':
          $ref: '#/components/responses/MailboxTooManyRequests'
        '503':
          $ref: '#/components/responses/MailboxUnavailable'
  /inbox/{file_name}:
    parameters:
    - $ref: '#/components/parameters/MailboxFileName'
    put:
      operationId: putMailboxFile
      summary: Upload an answers or statuses file
      description: 'Store an answers or statuses file (SPEC.md §9.2) for the requester to read, write-once
        by name (SPEC.md §9.4). The body is the file''s raw bytes. A new name is stored (201); the same
        bytes under a stored name are a retry (200, applied once); other bytes under a stored name are
        refused (409 FILE_NAME_TAKEN) and never overwrite the stored file, so new content needs a new
        name. Names MUST follow SPEC.md §9.2 for answers or statuses files. No Idempotency-Key: the name
        is the key.'
      tags:
      - Mailbox
      parameters:
      - $ref: '#/components/parameters/RelayAgent'
      requestBody:
        required: true
        content:
          text/csv:
            schema:
              type: string
            examples:
              answers_file:
                value: 'requester_id,provider_id,trip_id,leg_id,action,revision,result,status,provider_trip_id,scheduled_pickup,scheduled_dropoff,reason,answered_at

                  throughride-demo,county-dial-a-ride,tr-20261105-0001,leg-1,REQUEST,1,ACCEPTED,CONFIRMED,CDR-31877,2026-11-05T08:10:00-05:00,2026-11-05T08:55:00-05:00,,2026-11-04T15:40:00-05:00

                  throughride-demo,county-dial-a-ride,tr-20261105-0002,leg-1,REQUEST,1,ACCEPTED,CONFIRMED,CDR-31878,2026-11-05T09:05:00-05:00,,,2026-11-04T15:40:00-05:00

                  throughride-demo,county-dial-a-ride,tr-20261105-0003,leg-1,REQUEST,1,REJECTED,REJECTED,,,,OUTSIDE_SERVICE_AREA,2026-11-04T15:40:00-05:00

                  '
              with_row_problems:
                summary: An upload whose report lists row problems
                value: 'requester_id,provider_id,trip_id,leg_id,action,revision,result,status,provider_trip_id,scheduled_pickup,scheduled_dropoff,reason,answered_at

                  throughride-demo,county-dial-a-ride,tr-20261105-0001,leg-1,REQUEST,1,ACCEPTED,CONFIRMED,CDR-31877,2026-11-05T08:10:00-05:00,2026-11-05T08:55:00-05:00,,2026-11-04T15:40:00-05:00

                  throughride-demo,county-dial-a-ride,tr-20261105-0002,leg-1,REQUEST,1,ACCEPTED,CONFIRMED,CDR-31990,2026-11-05T09:05:00-05:00,,,2026-11-04T15:40:00-05:00

                  throughride-demo,county-dial-a-ride,tr-20261105-0009,leg-1,REQUEST,1,ACCEPTED,CONFIRMED,CDR-31901,2026-11-05T10:00:00-05:00,,,2026-11-04T16:10:00-05:00

                  throughride-demo,county-connector,tr-20261105-0003,leg-1,REQUEST,1,REJECTED,REJECTED,,,,OUTSIDE_SERVICE_AREA,2026-11-04T16:10:00-05:00

                  throughride-demo,county-dial-a-ride,tr-20261105-0003,leg-1,REQUEST,1,REJECTED,REJECTED,,,,OUTSIDE_SERVICE_AREA,yesterday

                  '
      responses:
        '200':
          description: The same bytes were already stored under this name; nothing changed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/mailbox-receipt'
              examples:
                retry:
                  value:
                    name: tds_answers_20261104T204000Z.csv
                    stored: false
                    sha256: eef2ac1723c655b97791b6abcf0e37944ccafe6e0d05bb9c104e52c36701e7cf
                    size: 611
                    received_at: '2026-11-04T20:40:05Z'
        '201':
          description: Stored; the requester will read it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/mailbox-receipt'
              examples:
                stored:
                  value:
                    name: tds_answers_20261104T204000Z.csv
                    stored: true
                    sha256: eef2ac1723c655b97791b6abcf0e37944ccafe6e0d05bb9c104e52c36701e7cf
                    size: 611
                    received_at: '2026-11-04T20:40:05Z'
        '400':
          description: The name is not an answers or statuses file name per SPEC.md §9.2 (code INVALID_FILE_NAME),
            or the body is empty (code EMPTY_FILE). The agent sets the file aside; repeating the call
            will not help.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/error'
              examples:
                bad_name:
                  value:
                    type: about:blank
                    title: Invalid file name
                    status: 400
                    detail: Upload answers or statuses files named as in SPEC.md §9.2, e.g. tds_answers_20261104T204000Z.csv.
                    code: INVALID_FILE_NAME
                empty:
                  value:
                    type: about:blank
                    title: Empty file
                    status: 400
                    detail: The upload has no bytes; a file needs at least its header row.
                    code: EMPTY_FILE
        '401':
          $ref: '#/components/responses/MailboxUnauthorized'
        '409':
          description: This name already holds other bytes (code FILE_NAME_TAKEN). The stored file is
            unchanged; export the new content under a new name.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/error'
              examples:
                other_bytes:
                  value:
                    type: about:blank
                    title: File name taken
                    status: 409
                    detail: tds_answers_20261104T204000Z.csv is already stored with other content; upload
                      it under a new name.
                    code: FILE_NAME_TAKEN
        '413':
          description: Larger than the mailbox's max_file_bytes (code FILE_TOO_LARGE). Split the rows
            over several files.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/error'
              examples:
                too_large:
                  value:
                    type: about:blank
                    title: File too large
                    status: 413
                    detail: Uploads are limited to 10485760 bytes; split the rows over several files.
                    code: FILE_TOO_LARGE
        '429':
          $ref: '#/components/responses/MailboxTooManyRequests'
        '503':
          $ref: '#/components/responses/MailboxUnavailable'
    get:
      operationId: getMailboxReport
      summary: Read what became of an uploaded file's rows
      description: 'The requester''s report on an answers or statuses file this agent uploaded (SPEC.md
        §9.4): RECEIVED until it is read, then PROCESSED with counts and an entry for every row not applied
        (DUPLICATE, or REJECTED with a code and, when one cell is at fault, its column), or REFUSED when
        the file could not be read at all. Only for requesters whose listing says `upload_reports: true`;
        others answer 404. Reports hold row numbers, column names and codes, never cell contents, and
        are kept at least 7 days after the file was read. Reading a report changes nothing.'
      tags:
      - Mailbox
      parameters:
      - $ref: '#/components/parameters/RelayAgent'
      responses:
        '200':
          description: The report.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/mailbox-report'
              examples:
                all_applied:
                  value:
                    name: tds_answers_20261104T204000Z.csv
                    sha256: eef2ac1723c655b97791b6abcf0e37944ccafe6e0d05bb9c104e52c36701e7cf
                    received_at: '2026-11-04T20:40:05Z'
                    status: PROCESSED
                    processed_at: '2026-11-04T20:41:00Z'
                    row_count: 3
                    applied_count: 3
                    duplicate_count: 0
                    rejected_count: 0
                    rows: []
                not_read_yet:
                  value:
                    name: tds_answers_20261104T211500Z.csv
                    sha256: 13751162071572e09e49f02cf629e372d88b65b703392ce5ea76c2be6d2a9ca1
                    received_at: '2026-11-04T21:15:05Z'
                    status: RECEIVED
                row_problems:
                  value:
                    name: tds_answers_20261104T211500Z.csv
                    sha256: 13751162071572e09e49f02cf629e372d88b65b703392ce5ea76c2be6d2a9ca1
                    received_at: '2026-11-04T21:15:05Z'
                    status: PROCESSED
                    processed_at: '2026-11-04T21:16:00Z'
                    row_count: 5
                    applied_count: 0
                    duplicate_count: 1
                    rejected_count: 4
                    rows:
                    - row: 2
                      outcome: DUPLICATE
                    - row: 3
                      outcome: REJECTED
                      code: KEY_REUSED
                    - row: 4
                      outcome: REJECTED
                      code: UNKNOWN_REQUEST
                      column: trip_id
                    - row: 5
                      outcome: REJECTED
                      code: WRONG_PROVIDER
                      column: provider_id
                    - row: 6
                      outcome: REJECTED
                      code: INVALID_VALUE
                      column: answered_at
                unreadable:
                  value:
                    name: tds_statuses_20261104T213000Z.csv
                    sha256: 9f2c1d0b7a6e5f4c3b2a19087f6e5d4c3b2a19087f6e5d4c3b2a19087f6e5d4c
                    received_at: '2026-11-04T21:30:02Z'
                    status: REFUSED
                    processed_at: '2026-11-04T21:31:00Z'
                    refusal: INVALID_HEADER
        '401':
          $ref: '#/components/responses/MailboxUnauthorized'
        '404':
          description: No upload with this name in this mailbox, its report has expired, or the requester
            does not report on uploads (code NOT_FOUND).
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/error'
              examples:
                unknown_upload:
                  value:
                    type: about:blank
                    title: Not found
                    status: 404
                    detail: No uploaded file tds_answers_20261103T090000Z.csv in this mailbox, or its
                      report has expired.
                    code: NOT_FOUND
        '429':
          $ref: '#/components/responses/MailboxTooManyRequests'
        '503':
          $ref: '#/components/responses/MailboxUnavailable'
components:
  securitySchemes:
    mailboxToken:
      type: http
      scheme: bearer
      description: A token the requester issues for one provider's mailbox and hands to the provider's
        staff out of band (SPEC.md §9.4, §12). It reaches that mailbox only. Agents keep it out of configuration
        files that others can read and out of logs. A requester MAY accept an OAuth 2.0 access token here
        instead; the agent sends it the same way.
  parameters:
    RelayAgent:
      name: X-Relay-Agent
      in: header
      required: false
      description: Optional `<agent name>/<version>` of the calling agent (e.g. `ridesheet-tds-connector/0.1.0`),
        shown to the requester's operators. A header of its own because some agent platforms (Google Apps
        Script) cannot set User-Agent. Carries no rider data.
      schema:
        type: string
        maxLength: 80
    MailboxFileName:
      name: file_name
      in: path
      required: true
      description: 'A file name in the relay mailbox (SPEC.md §9.4): letters, digits, `_`, `-` and `.`
        only, as the CSV profile names files (SPEC.md §9.2). Scoped to the mailbox the credential names.'
      schema:
        type: string
        minLength: 1
        maxLength: 128
  schemas:
    mailbox-file:
      title: MailboxFile
      description: 'One request file waiting in a relay mailbox (SPEC.md §9.4): its name, size and SHA-256,
        so an agent can fetch it, check it and skip a file it already has. The bytes themselves come from
        GET /outbox/{file_name}.'
      x-profile: csv
      type: object
      required:
      - name
      - size
      - sha256
      - created_at
      properties:
        name:
          description: File name, following SPEC.md §9.2 for a requests file (`tds_requests_<YYYYMMDDTHHMMSSZ>[_<tag>].csv`).
            Unique within the mailbox; never reused for other bytes.
          type: string
          pattern: ^tds_requests_[0-9]{8}T[0-9]{6}Z(_[A-Za-z0-9-]{1,40})?\.csv$
        size:
          description: Length of the file in bytes.
          type: integer
          minimum: 1
        sha256:
          description: SHA-256 of the file's bytes, lowercase hex. Same value as the X-Relay-SHA256 header
            on download.
          type: string
          pattern: ^[0-9a-f]{64}$
        created_at:
          description: When the requester put the file in the mailbox (RFC 3339).
          type: string
          format: date-time
    mailbox-listing:
      title: MailboxListing
      description: 'Answer to GET /outbox (SPEC.md §9.4): the request files the agent has not acknowledged
        yet, oldest first, and how often the requester expects the agent to call. An empty `files` list
        is the normal answer when nothing is waiting.'
      x-profile: csv
      type: object
      required:
      - provider_id
      - poll_interval_s
      - files
      properties:
        provider_id:
          description: The provider whose mailbox this is, taken from the credential. The same id the
            provider uses in the files' `provider_id` column.
          type: string
          minLength: 1
        poll_interval_s:
          description: Seconds the requester expects between an agent's calls. Agents SHOULD poll about
            this often and no more often, apart from retries; a requester MAY report an agent as silent
            after three intervals without a call.
          type: integer
          minimum: 1
        requester_id:
          description: The requester serving the mailbox, as it appears in the files' `requester_id` column.
            Lets an agent check its answers before uploading.
          type: string
          minLength: 1
        upload_reports:
          description: 'True when GET /inbox/{file_name} reports what became of each uploaded file''s
            rows (SPEC.md §9.4). Absent or false: the requester does not report, and that call is 404.'
          type: boolean
        max_file_bytes:
          description: Largest upload, in bytes, PUT /inbox/{file_name} accepts; larger files are refused
            with FILE_TOO_LARGE (413). At least 1000000 when given.
          type: integer
          minimum: 1000000
        files:
          description: Request files not yet acknowledged, oldest first (by created_at, then name).
          type: array
          items:
            $ref: '#/components/schemas/mailbox-file'
    refusal-reason:
      title: RefusalReason
      description: 'Why a provider refuses to serve a trip, tender or change. Used as the `code` of a
        422 Problem Details response and as the `reason` on a rejected tender. ELIGIBILITY_REQUIRED: the
        trip needs an eligibility attestation the request does not carry. ELIGIBILITY_NOT_ACCEPTED: the
        request carries one, but the provider does not accept its attesting party or verification method
        (SPEC.md §6.5.1); the detail may say which, never anything about the rider. OUTSIDE_BOOKING_WINDOW:
        the request came too early or too late for the provider''s booking window (SPEC.md §7.1, `/health`
        booking_window); when too early, the Problem Details carry `bookable_from`. A later request is
        a new trip (new trip_id).'
      type: string
      enum:
      - NO_CAPACITY
      - OUTSIDE_SERVICE_AREA
      - OUTSIDE_SERVICE_HOURS
      - ELIGIBILITY_REQUIRED
      - ELIGIBILITY_NOT_ACCEPTED
      - OUTSIDE_BOOKING_WINDOW
      - INVALID_WINDOW
      - UNSUPPORTED_REQUIREMENT
    error:
      title: Error
      description: RFC 9457 Problem Details (media type application/problem+json) with a machine-readable
        `code` so requesters can react without parsing text.
      type: object
      required:
      - title
      - status
      - code
      properties:
        type:
          description: URI identifying the problem type. Defaults to "about:blank".
          type: string
          format: uri-reference
          default: about:blank
        title:
          description: Short human-readable summary of the problem type.
          type: string
        status:
          description: HTTP status code generated by the origin server.
          type: integer
          minimum: 100
          maximum: 599
        detail:
          description: Human-readable explanation specific to this occurrence.
          type: string
        instance:
          description: URI reference identifying this occurrence.
          type: string
          format: uri-reference
        code:
          description: 'Machine-readable reason: a protocol error, or a RefusalReason (a 422 with one
            of these leaves the trip REJECTED). TRIP_ID_IN_USE also covers a reused tender_id or hold_id.
            RATE_LIMITED (429) and TEMPORARILY_UNAVAILABLE (503) are back-pressure (SPEC.md §7.2): the
            request was not processed and may be retried with the same Idempotency-Key after retry_after_s.
            INVALID_FILE_NAME, EMPTY_FILE, FILE_TOO_LARGE and FILE_NAME_TAKEN are a relay mailbox''s refusals
            of an upload (SPEC.md §9.4); NOT_FOUND there means no such file in this mailbox. Unknown values
            must be treated as a generic failure.'
          anyOf:
          - type: string
            enum:
            - INVALID_REQUEST
            - UNAUTHORIZED
            - NOT_FOUND
            - INVALID_STATE
            - IDEMPOTENCY_KEY_REUSED
            - TRIP_ID_IN_USE
            - RATE_LIMITED
            - TEMPORARILY_UNAVAILABLE
            - INVALID_FILE_NAME
            - EMPTY_FILE
            - FILE_TOO_LARGE
            - FILE_NAME_TAKEN
          - $ref: '#/components/schemas/refusal-reason'
        bookable_from:
          description: 'With code OUTSIDE_BOOKING_WINDOW, when the request came too early: the first moment
            a request for this trip will be inside the provider''s booking window (trip time minus max_advance_s).
            Absent when waiting will not help (too little notice).'
          type: string
          format: date-time
        retry_after_s:
          description: 'Back-pressure hint (SPEC.md §7.2): the least number of seconds the client should
            wait before retrying this request. Same value as the Retry-After response header (RFC 9110
            §10.2.3), for clients that cannot read headers; when both are sent they SHOULD agree, and
            a client that sees both uses the larger. A provider SHOULD send it with 429 and 503, and MAY
            send it with any other 5xx. The retry repeats the same request with the same Idempotency-Key.'
          type: integer
          minimum: 0
    mailbox-ack:
      title: MailboxAck
      description: 'Answer to POST /ack/{file_name} (SPEC.md §9.4): the request file has been taken and
        no longer appears in the listing. Acknowledging again returns the same answer, with the time of
        the first acknowledgement.'
      x-profile: csv
      type: object
      required:
      - name
      - acked_at
      properties:
        name:
          description: The acknowledged file's name.
          type: string
          minLength: 1
        acked_at:
          description: When the file was first acknowledged (RFC 3339). Later acknowledgements do not
            change it.
          type: string
          format: date-time
    row-problem:
      title: CsvRowProblem
      description: 'Why a requester did not apply one row of an uploaded answers or statuses file (SPEC.md
        §9.4). One code per row; when several apply, the first in this order. Unknown values must be treated
        as OTHER. MISSING_VALUE: a required cell is empty or its column is missing. INVALID_VALUE: a cell
        does not have its column''s type or allowed values, or the row does not stand for a valid API
        object (§9.2 mapping). WRONG_PROVIDER: provider_id is not the provider whose mailbox received
        the file. WRONG_REQUESTER: requester_id is not this requester. UNKNOWN_REQUEST: an answer row
        whose key matches no request row this requester sent. UNKNOWN_TRIP: a status row about a trip
        and leg this requester did not request from the provider. KEY_REUSED: the row''s key (answers:
        the request key; statuses: provider_id and event_id) was applied before with different content
        (§9.2). INVALID_STATE: the row cannot apply to the trip as it stands, e.g. a milestone out of
        order (§10.3) or a change to a finished trip. OTHER: any other reason.'
      x-profile: csv
      type: string
      enum:
      - MISSING_VALUE
      - INVALID_VALUE
      - WRONG_PROVIDER
      - WRONG_REQUESTER
      - UNKNOWN_REQUEST
      - UNKNOWN_TRIP
      - KEY_REUSED
      - INVALID_STATE
      - OTHER
    mailbox-row-outcome:
      title: MailboxRowOutcome
      description: 'What the requester did with one row of an uploaded file (SPEC.md §9.4). A report lists
        every row that was not applied; rows it does not list were applied. Never carries cell contents:
        only the row number, the column name and a code.'
      x-profile: csv
      type: object
      required:
      - row
      - outcome
      properties:
        row:
          description: 'The row''s position in the file: the header is row 1 and the first data row is
            2. Records are counted, not physical lines (a quoted line break does not start a new row),
            and rows with no cells at all are skipped.'
          type: integer
          minimum: 2
        outcome:
          description: 'DUPLICATE: the same row (same key and content) was already applied, from this
            file or an earlier one, so it changed nothing (§9.2). REJECTED: the row was not applied; `code`
            says why. APPLIED rows are not listed.'
          type: string
          enum:
          - DUPLICATE
          - REJECTED
        code:
          description: Why a REJECTED row was not applied. Required for REJECTED, absent for DUPLICATE.
          $ref: '#/components/schemas/row-problem'
        column:
          description: The column at fault, by header name, when one cell is the cause (e.g. `answered_at`
            for INVALID_VALUE, `provider_id` for WRONG_PROVIDER).
          type: string
          minLength: 1
      allOf:
      - if:
          properties:
            outcome:
              const: REJECTED
        then:
          required:
          - code
        else:
          not:
            required:
            - code
    mailbox-report:
      title: MailboxReport
      description: 'Answer to GET /inbox/{file_name} (SPEC.md §9.4): what the requester did with an uploaded
        answers or statuses file, so the provider''s agent can show its staff why a row did not land.
        RECEIVED: stored, not read yet. PROCESSED: read; the counts add up to `row_count`, and `rows`
        lists every row not applied (DUPLICATE or REJECTED), in file order. REFUSED: the file as a whole
        could not be read (`refusal`), so no row was applied. A report holds row numbers, column names
        and codes only, never cell contents.'
      x-profile: csv
      type: object
      required:
      - name
      - sha256
      - received_at
      - status
      properties:
        name:
          description: The uploaded file's name.
          type: string
          minLength: 1
        sha256:
          description: SHA-256 of the stored bytes, lowercase hex, as in the upload receipt.
          type: string
          pattern: ^[0-9a-f]{64}$
        received_at:
          description: When the file was first stored (RFC 3339).
          type: string
          format: date-time
        status:
          description: RECEIVED (not read yet), PROCESSED (read row by row) or REFUSED (unreadable as
            a whole).
          type: string
          enum:
          - RECEIVED
          - PROCESSED
          - REFUSED
        processed_at:
          description: When the requester read the file (RFC 3339). Present once PROCESSED or REFUSED.
          type: string
          format: date-time
        refusal:
          description: 'Why a REFUSED file could not be read: INVALID_ENCODING (not UTF-8), INVALID_HEADER
            (no header row, a duplicate or unnamed column, or a required column missing), OTHER. Unknown
            values must be treated as OTHER.'
          type: string
          enum:
          - INVALID_ENCODING
          - INVALID_HEADER
          - OTHER
        row_count:
          description: Data rows read (the header and rows with no cells not counted).
          type: integer
          minimum: 0
        applied_count:
          description: Rows applied.
          type: integer
          minimum: 0
        duplicate_count:
          description: Rows that repeated a row already applied and changed nothing.
          type: integer
          minimum: 0
        rejected_count:
          description: Rows not applied; each is listed in `rows` with its code.
          type: integer
          minimum: 0
        rows:
          description: Every row not applied, in file order. Empty when every row was applied.
          type: array
          items:
            $ref: '#/components/schemas/mailbox-row-outcome'
      allOf:
      - if:
          properties:
            status:
              const: PROCESSED
        then:
          required:
          - processed_at
          - row_count
          - applied_count
          - duplicate_count
          - rejected_count
          - rows
      - if:
          properties:
            status:
              const: REFUSED
        then:
          required:
          - processed_at
          - refusal
        else:
          not:
            required:
            - refusal
    mailbox-receipt:
      title: MailboxReceipt
      description: 'Answer to PUT /inbox/{file_name} (SPEC.md §9.4). `stored: true` (201): the file is
        new and the requester will read it. `stored: false` (200): the same bytes were already stored
        under this name, so the upload was a retry and is applied once. Other bytes under a stored name
        are refused (409 FILE_NAME_TAKEN), never overwritten.'
      x-profile: csv
      type: object
      required:
      - name
      - stored
      - sha256
      properties:
        name:
          description: The stored file's name.
          type: string
          minLength: 1
        stored:
          description: True when this upload stored the file; false when the same bytes were already there.
          type: boolean
        sha256:
          description: SHA-256 of the stored bytes, lowercase hex, so the agent can confirm what arrived.
          type: string
          pattern: ^[0-9a-f]{64}$
        size:
          description: Length of the stored file in bytes.
          type: integer
          minimum: 1
        received_at:
          description: When the file was first stored (RFC 3339). A retry does not change it.
          type: string
          format: date-time
  headers:
    RetryAfter:
      description: 'Back-pressure hint (RFC 9110 §10.2.3; SPEC.md §7.2): how long the client should wait
        before retrying, as delta-seconds (e.g. `20`) or an HTTP-date (e.g. `Sat, 24 Oct 2026 14:05:00
        GMT`). Providers SHOULD send delta-seconds. The Problem Details body repeats the wait as `retry_after_s`.
        Clients treat it as a minimum: wait at least this long, then retry the same request with the same
        Idempotency-Key. A value that cannot be parsed is ignored.'
      schema:
        type: string
        pattern: ^([0-9]+|(Mon|Tue|Wed|Thu|Fri|Sat|Sun), [0-9]{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)
          [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} GMT)$
      example: '20'
    RelaySHA256:
      description: SHA-256 of the response body, lowercase hex (SPEC.md §9.4). Equals the `sha256` the
        listing gave for the file; an agent that computes another value discards the download and fetches
        it again on its next poll.
      schema:
        type: string
        pattern: ^[0-9a-f]{64}$
      example: 57dd4befc5ddd8a97df0ab5cff0fd86282dccf3a5e3990db4d804cff57e3b673
  responses:
    MailboxTooManyRequests:
      description: 'Back-pressure (SPEC.md §7.2): the agent is calling too often (code RATE_LIMITED).
        Nothing was processed. Repeat the same call after the wait in Retry-After / retry_after_s.'
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/error'
    MailboxUnauthorized:
      description: 'Missing or invalid mailbox token, or a credential that names no relay mailbox (code
        UNAUTHORIZED). A requester MAY answer 403 with the same code for a valid credential that is not
        a mailbox token; agents treat both the same way: stop and ask a person to fix the settings.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/error'
    MailboxUnavailable:
      description: 'Back-pressure (SPEC.md §7.2): the mailbox cannot take calls for now (code TEMPORARILY_UNAVAILABLE).
        Nothing was processed. Repeat the same call after the wait in Retry-After / retry_after_s.'
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/error'
