openapi: 3.1.0
info:
  title: QuickDispatch Customer API
  version: 1.0.0
  license:
    name: Proprietary. Use with a QuickDispatch account.
    url: https://quickdispatch.co.uk/terms
  contact:
    name: QuickDispatch developers
    email: hello@quickdispatch.co.uk
    url: https://quickdispatch.co.uk/developers/
  description: |
    The Customer API is for **shippers**: the retailers and brands whose goods a QuickDispatch courier delivers.
    Use it to send consignments to your courier, follow every piece from depot scan to doorstep, and fetch proof of delivery.

    Your courier issues your API keys from their QuickDispatch account. Couriers managing their own operation should use the
    [Admin API](admin-api.html) instead.

    ## Base URLs

    | Environment | Base URL |
    |---|---|
    | Live | `https://api.quickdispatch.co.uk/v1` |
    | Sandbox | `https://sandbox.api.quickdispatch.co.uk/v1` |

    Sandbox keys start `qd_test_` and never send real texts or emails. Live keys start `qd_live_`.

    ## Authentication

    Send your key as a bearer token on every request:

    ```
    Authorization: Bearer qd_live_xxxxxxxxxxxxxxxxxxxx
    ```

    Keys are scoped to one shipper account at one courier. Never put a key in browser or mobile app code.

    ## Idempotency

    Every `POST` accepts an `Idempotency-Key` header (any unique string up to 64 characters, a UUID works well).
    If a request times out, retry it with the same key: we return the original result instead of creating a duplicate.
    Keys are remembered for 24 hours.

    ## Pagination

    List endpoints return up to `limit` items (default 25, maximum 100) newest first, with `has_more`.
    To get the next page, pass the `id` of the last item as `starting_after`.

    ## Errors

    Errors use standard HTTP status codes and always return the same shape:

    ```json
    { "error": { "code": "invalid_postcode", "message": "recipient.address.postcode is not a valid UK postcode",
                 "param": "recipient.address.postcode", "request_id": "req_8h2k1m0q3x7z" } }
    ```

    Quote the `request_id` (also in the `X-Request-Id` header) when you contact support.

    ## Rate limits

    120 requests per minute per key. Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`.
    Over the limit you get `429` with a `Retry-After` header in seconds.

    ## Consignment lifecycle

    | Status | Meaning |
    |---|---|
    | `awaiting_scan` | Created. No pieces have arrived at the depot yet. |
    | `part_scanned` | Some pieces have been scanned in at the depot. |
    | `scanned` | Every piece is at the depot. |
    | `awaiting_reply` | A booking request has gone to the recipient by SMS and email. |
    | `booked` | The recipient has a delivery slot. |
    | `out_for_delivery` | On a van today. A live tracking link is active. |
    | `delivered` | Delivered, with proof of delivery. |
    | `failed_attempt` | An attempt failed. The recipient is sent a rebooking request automatically. |
    | `cancelled` | Cancelled before dispatch. |
    | `returned` | Returned to the depot or to you. |

    ## Webhooks

    Register an endpoint to receive events instead of polling. Each delivery is a `POST` with a JSON body and a
    `QD-Signature` header: `t=<unix timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>" using your endpoint secret>`.
    Reject events older than 5 minutes. We retry failed deliveries with back-off for 24 hours.
servers:
  - url: https://api.quickdispatch.co.uk/v1
    description: Live
  - url: https://sandbox.api.quickdispatch.co.uk/v1
    description: Sandbox
security:
  - apiKey: []
tags:
  - name: Consignments
    description: A consignment is one delivery to one recipient. It holds the items and pieces, the booked slot and its status.
  - name: Pieces
    description: The individual physical parcels in a consignment. Each piece is scanned at the depot, onto the van and at the door.
  - name: Tracking
    description: Where a consignment is now and everything that has happened to it.
  - name: Proof of delivery
    description: Signature, photos and GPS stamp captured by the driver.
  - name: Collections
    description: Collection-only jobs, such as returns or exchanges, picked up from a recipient.
  - name: Webhook endpoints
    description: Where we send events about your consignments.
  - name: Account
    description: The shipper account your key belongs to.
x-tagGroups:
  - name: Deliveries
    tags: [Consignments, Pieces, Tracking, Proof of delivery, Collections]
  - name: Setup
    tags: [Webhook endpoints, Account]
paths:
  /consignments:
    post:
      tags: [Consignments]
      operationId: createConsignment
      summary: Create a consignment
      description: |
        Sends a new delivery to your courier. The recipient is asked to book a slot by SMS and email once every piece has been
        scanned at the depot, unless you pass a `booking.slot` that you have already agreed with them.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConsignmentCreate'
            example:
              reference: OAK-48213
              recipient:
                name: Sarah Khan
                phone: '+447700900123'
                email: sarah@example.com
                address: { line1: 14 Headingley Avenue, town: Leeds, postcode: LS6 2AB, country: GB }
              items:
                - { sku: DIVAN-4FT6-GREY, description: 'Divan base 4ft6 Grey', pieces: 2, weight_kg: 58, volume_m3: 0.9 }
                - { sku: HB-4FT6-GREY, description: 'Headboard 4ft6 Grey', pieces: 1, weight_kg: 9, volume_m3: 0.15 }
              service: { crew: 2, assembly: true, remove_packaging: true }
              booking: { channels: [sms, email] }
              notes: Ground floor flat, side door.
      responses:
        '201':
          description: Consignment created.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Consignment' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
    get:
      tags: [Consignments]
      operationId: listConsignments
      summary: List consignments
      parameters:
        - name: status
          in: query
          description: Only consignments in this status. Repeat the parameter for several.
          schema: { $ref: '#/components/schemas/ConsignmentStatus' }
        - name: reference
          in: query
          description: Your own order reference.
          schema: { type: string }
        - name: updated_since
          in: query
          description: Only consignments changed after this time. Useful for syncing.
          schema: { type: string, format: date-time }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/StartingAfter'
      responses:
        '200':
          description: A page of consignments.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConsignmentList' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /consignments/{consignment_id}:
    parameters:
      - $ref: '#/components/parameters/ConsignmentId'
    get:
      tags: [Consignments]
      operationId: getConsignment
      summary: Retrieve a consignment
      responses:
        '200':
          description: The consignment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Consignment' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Consignments]
      operationId: updateConsignment
      summary: Update a consignment
      description: |
        Change recipient details, notes or items. Items can only change while the status is `awaiting_scan`.
        Recipient details can change until the consignment is `out_for_delivery`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ConsignmentUpdate' }
            example:
              recipient: { phone: '+447700900456' }
              notes: Please call on arrival.
      responses:
        '200':
          description: The updated consignment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Consignment' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }
  /consignments/{consignment_id}/cancel:
    parameters:
      - $ref: '#/components/parameters/ConsignmentId'
    post:
      tags: [Consignments]
      operationId: cancelConsignment
      summary: Cancel a consignment
      description: Cancels a consignment that is not yet `out_for_delivery`. The recipient is told by SMS if they had booked.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, maxLength: 200, examples: [Customer cancelled the order] }
      responses:
        '200':
          description: The cancelled consignment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Consignment' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
  /consignments/{consignment_id}/pieces:
    parameters:
      - $ref: '#/components/parameters/ConsignmentId'
    get:
      tags: [Pieces]
      operationId: listPieces
      summary: List pieces and their scans
      responses:
        '200':
          description: Every piece with its latest scan.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Piece' }
                  scanned: { type: integer, examples: [2] }
                  expected: { type: integer, examples: [3] }
        '404': { $ref: '#/components/responses/NotFound' }
  /consignments/{consignment_id}/tracking:
    parameters:
      - $ref: '#/components/parameters/ConsignmentId'
    get:
      tags: [Tracking]
      operationId: getTracking
      summary: Live tracking
      description: The current position and arrival window. Location is only returned while the consignment is `out_for_delivery`.
      responses:
        '200':
          description: The tracking snapshot.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Tracking' }
        '404': { $ref: '#/components/responses/NotFound' }
  /consignments/{consignment_id}/events:
    parameters:
      - $ref: '#/components/parameters/ConsignmentId'
    get:
      tags: [Tracking]
      operationId: listEvents
      summary: List events
      description: The full timeline, oldest first.
      responses:
        '200':
          description: The events.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Event' }
        '404': { $ref: '#/components/responses/NotFound' }
  /consignments/{consignment_id}/pod:
    parameters:
      - $ref: '#/components/parameters/ConsignmentId'
    get:
      tags: [Proof of delivery]
      operationId: getProofOfDelivery
      summary: Retrieve proof of delivery
      description: Available once the consignment is `delivered`. Photo and signature URLs are signed and expire after 1 hour.
      responses:
        '200':
          description: Proof of delivery.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProofOfDelivery' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: The consignment has not been delivered yet.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /collections:
    post:
      tags: [Collections]
      operationId: createCollection
      summary: Book a collection
      description: Creates a collection-only job, for example a return. The recipient is asked to book a collection slot.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CollectionCreate' }
            example:
              reference: OAK-48213-RTN
              original_consignment: con_8Kq2Lm
              recipient:
                name: Sarah Khan
                phone: '+447700900123'
                address: { line1: 14 Headingley Avenue, town: Leeds, postcode: LS6 2AB, country: GB }
              items:
                - { sku: HB-4FT6-GREY, description: 'Headboard 4ft6 Grey', pieces: 1 }
              reason: Changed mind
      responses:
        '201':
          description: Collection created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Consignment' }
        '422': { $ref: '#/components/responses/Unprocessable' }
  /webhook-endpoints:
    get:
      tags: [Webhook endpoints]
      operationId: listWebhookEndpoints
      summary: List webhook endpoints
      responses:
        '200':
          description: Your endpoints.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/WebhookEndpoint' }
    post:
      tags: [Webhook endpoints]
      operationId: createWebhookEndpoint
      summary: Add a webhook endpoint
      description: The response includes the signing `secret`. It is only shown once, so store it safely.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri, examples: ['https://shop.example.com/hooks/quickdispatch'] }
                events:
                  type: array
                  items: { $ref: '#/components/schemas/EventType' }
      responses:
        '201':
          description: Endpoint created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/WebhookEndpoint'
                  - type: object
                    properties:
                      secret: { type: string, examples: [whsec_3f9b2c7d1e8a4f60] }
        '422': { $ref: '#/components/responses/Unprocessable' }
  /webhook-endpoints/{endpoint_id}:
    parameters:
      - name: endpoint_id
        in: path
        required: true
        schema: { type: string, examples: [we_4Tn1] }
    delete:
      tags: [Webhook endpoints]
      operationId: deleteWebhookEndpoint
      summary: Delete a webhook endpoint
      responses:
        '204': { description: 'Deleted.' }
        '404': { $ref: '#/components/responses/NotFound' }
  /me:
    get:
      tags: [Account]
      operationId: getAccount
      summary: Retrieve your account
      responses:
        '200':
          description: The shipper account for this key.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Account' }
webhooks:
  consignment.scanned:
    post:
      summary: Pieces scanned at the depot
      description: Sent each time a piece is scanned in. `data.status` is `part_scanned` or `scanned`.
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '200': { description: 'Return any 2xx within 10 seconds to acknowledge.' }
  consignment.booked:
    post:
      summary: Delivery slot booked
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '200': { description: 'Acknowledged.' }
  consignment.out_for_delivery:
    post:
      summary: Out for delivery
      description: Includes `data.tracking_url`, which you can pass on to your customer.
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '200': { description: 'Acknowledged.' }
  consignment.delivered:
    post:
      summary: Delivered
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '200': { description: 'Acknowledged.' }
  consignment.failed_attempt:
    post:
      summary: Delivery attempt failed
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '200': { description: 'Acknowledged.' }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: API key (qd_live_… or qd_test_…)
  parameters:
    ConsignmentId:
      name: consignment_id
      in: path
      required: true
      description: The QuickDispatch consignment ID, e.g. `con_8Kq2Lm`.
      schema: { type: string, examples: [con_8Kq2Lm] }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: A unique string so retries never create duplicates. Remembered for 24 hours.
      schema: { type: string, maxLength: 64, examples: [5f1c44a2-9d0e-4b7a-8a31-2c6e0f4b9d11] }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    StartingAfter:
      name: starting_after
      in: query
      description: The `id` of the last item on the previous page.
      schema: { type: string }
  headers:
    X-Request-Id:
      description: Unique ID for this request. Quote it to support.
      schema: { type: string, examples: [req_8h2k1m0q3x7z] }
  responses:
    BadRequest:
      description: The request was malformed.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: invalid_request, message: 'items[0].pieces must be at least 1', param: 'items[0].pieces', request_id: req_8h2k1m0q3x7z } }
    Unauthorized:
      description: Missing, revoked or invalid API key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: invalid_api_key, message: The API key is revoked or does not exist, request_id: req_2b7x0k9w1m3p } }
    NotFound:
      description: No such resource for this account.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: The request conflicts with the current state, such as a duplicate reference or a change after dispatch.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: duplicate_reference, message: A consignment with this reference already exists, param: reference, request_id: req_0q2m8x1k7h3z } }
    Unprocessable:
      description: The request was valid JSON but a value was not accepted.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: invalid_postcode, message: recipient.address.postcode is not a valid UK postcode, param: recipient.address.postcode, request_id: req_8h2k1m0q3x7z } }
    RateLimited:
      description: Too many requests.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: { type: integer, examples: [14] }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code: { type: string, description: 'Machine-readable error code.', examples: [invalid_postcode] }
            message: { type: string, description: 'What went wrong, in plain English.' }
            param: { type: string, description: 'The field the error relates to, if any.' }
            request_id: { type: string }
    Address:
      type: object
      required: [line1, town, postcode]
      properties:
        line1: { type: string, examples: [14 Headingley Avenue] }
        line2: { type: string }
        town: { type: string, examples: [Leeds] }
        county: { type: string }
        postcode: { type: string, examples: [LS6 2AB] }
        country: { type: string, default: GB, description: 'ISO 3166-1 alpha-2.' }
        what3words: { type: string, examples: [///index.home.raft] }
        latitude: { type: number, readOnly: true }
        longitude: { type: number, readOnly: true }
    Recipient:
      type: object
      required: [name, phone, address]
      properties:
        name: { type: string, examples: [Sarah Khan] }
        phone: { type: string, description: 'E.164 format. Used for booking and tracking texts.', examples: ['+447700900123'] }
        email: { type: string, format: email }
        address: { $ref: '#/components/schemas/Address' }
    Item:
      type: object
      required: [description, pieces]
      properties:
        sku: { type: string, examples: [DIVAN-4FT6-GREY] }
        description: { type: string, examples: [Divan base 4ft6 Grey] }
        pieces: { type: integer, minimum: 1, description: 'How many separate physical parcels this item arrives in.', examples: [2] }
        weight_kg: { type: number, examples: [58] }
        volume_m3: { type: number, examples: [0.9] }
    Piece:
      type: object
      properties:
        id: { type: string, examples: [pc_19Xb] }
        barcode: { type: string, examples: [QD8KQ2LM0102] }
        item_sku: { type: string }
        number: { type: string, description: 'Piece number within the consignment.', examples: [2 of 3] }
        status: { type: string, enum: [awaiting_scan, at_depot, on_van, delivered, missing, damaged] }
        last_scan:
          type: [object, 'null']
          properties:
            at: { type: string, format: date-time }
            location: { type: string, examples: [Leeds depot] }
            by: { type: string, examples: [Depot scanner 2] }
    ConsignmentStatus:
      type: string
      enum: [awaiting_scan, part_scanned, scanned, awaiting_reply, booked, out_for_delivery, delivered, failed_attempt, cancelled, returned, collected, debriefed]
    Service:
      type: object
      properties:
        crew: { type: integer, enum: [1, 2], default: 1, description: 'Two-person crew for heavy or bulky goods.' }
        assembly: { type: boolean, default: false }
        remove_packaging: { type: boolean, default: false }
        disposal: { type: boolean, default: false, description: 'Take away an old item.' }
    Slot:
      type: object
      properties:
        date: { type: string, format: date, examples: ['2026-10-16'] }
        from: { type: string, examples: ['12:00'] }
        to: { type: string, examples: ['16:00'] }
    Consignment:
      type: object
      properties:
        id: { type: string, readOnly: true, examples: [con_8Kq2Lm] }
        object: { type: string, const: consignment }
        reference: { type: string, examples: [OAK-48213] }
        type: { type: string, enum: [delivery, return, exchange], description: 'delivery takes items to the recipient. return collects items and brings them back to the depot. exchange does both in one visit.' }
        status: { $ref: '#/components/schemas/ConsignmentStatus' }
        recipient: { $ref: '#/components/schemas/Recipient' }
        items:
          type: array
          items: { $ref: '#/components/schemas/Item' }
        collect_items:
          type: array
          description: Items collected from the recipient (return and exchange only). Each collected piece gets a collection barcode ending in R.
          items: { $ref: '#/components/schemas/Item' }
        return_reason: { type: [string, 'null'], examples: ['Wrong size or colour'] }
        after_debrief: { type: [string, 'null'], enum: [stock, hold, dispose, null], description: 'What you asked the depot to do once the collected items are checked.' }
        debrief: { oneOf: [{ $ref: '#/components/schemas/Debrief' }, { type: 'null' }] }
        pieces: { type: object, properties: { expected: { type: integer, examples: [3] }, scanned: { type: integer, examples: [3] } } }
        service: { $ref: '#/components/schemas/Service' }
        booking:
          type: object
          properties:
            slot: { oneOf: [{ $ref: '#/components/schemas/Slot' }, { type: 'null' }] }
            requested_at: { type: [string, 'null'], format: date-time }
            booked_at: { type: [string, 'null'], format: date-time }
            booked_by: { type: [string, 'null'], enum: [recipient, courier, shipper, null] }
        tracking_url: { type: [string, 'null'], format: uri, examples: ['https://track.quickdispatch.co.uk/8Kq2Lm'] }
        notes: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Debrief:
      type: object
      description: The depot's check of collected pieces.
      properties:
        debriefed_at: { type: string, format: date-time }
        condition: { type: string, enum: [as_new_unopened, opened_as_new, minor_marks, damaged, missing_parts] }
        pieces_received: { type: integer, examples: [2] }
        notes: { type: string }
        photos: { type: array, items: { type: string, format: uri } }
        outcome: { type: string, enum: [stock, hold, repair, dispose] }
        bay: { type: string, examples: [R7] }
    ConsignmentCreate:
      type: object
      required: [reference, recipient, items]
      properties:
        reference: { type: string, maxLength: 64, description: 'Your order reference. Must be unique on your account.' }
        type: { type: string, enum: [delivery, return, exchange], default: delivery }
        recipient: { $ref: '#/components/schemas/Recipient' }
        items:
          type: array
          minItems: 1
          description: Items to deliver. For a return, list the items to collect here instead.
          items: { $ref: '#/components/schemas/Item' }
        collect_items:
          type: array
          description: Exchange only. The items to collect. Leave it out to collect the same items you're delivering.
          items: { $ref: '#/components/schemas/Item' }
        return_reason: { type: string, maxLength: 200 }
        after_debrief: { type: string, enum: [stock, hold, dispose], default: stock, description: 'stock sends it back to your warehouse, hold keeps it at the depot for re-delivery, dispose needs your approval after the condition report.' }
        service: { $ref: '#/components/schemas/Service' }
        booking:
          type: object
          properties:
            channels:
              type: array
              items: { type: string, enum: [sms, email] }
              default: [sms, email]
            slot:
              $ref: '#/components/schemas/Slot'
              description: Only if you have already agreed a slot with the recipient. Subject to your courier's capacity.
        notes: { type: string, maxLength: 500 }
    ConsignmentUpdate:
      type: object
      properties:
        recipient:
          type: object
          properties:
            name: { type: string }
            phone: { type: string }
            email: { type: string, format: email }
            address: { $ref: '#/components/schemas/Address' }
        items:
          type: array
          items: { $ref: '#/components/schemas/Item' }
        notes: { type: string, maxLength: 500 }
    ConsignmentList:
      type: object
      properties:
        object: { type: string, const: list }
        data:
          type: array
          items: { $ref: '#/components/schemas/Consignment' }
        has_more: { type: boolean }
    CollectionCreate:
      type: object
      required: [reference, recipient, items]
      properties:
        reference: { type: string }
        original_consignment: { type: string, description: 'The delivery this collection relates to, if any.' }
        recipient: { $ref: '#/components/schemas/Recipient' }
        items:
          type: array
          items: { $ref: '#/components/schemas/Item' }
        reason: { type: string, examples: [Changed mind] }
    Tracking:
      type: object
      properties:
        consignment: { type: string, examples: [con_8Kq2Lm] }
        status: { $ref: '#/components/schemas/ConsignmentStatus' }
        stops_away: { type: [integer, 'null'], examples: [6] }
        eta: { type: [object, 'null'], properties: { from: { type: string, examples: ['13:40'] }, to: { type: string, examples: ['14:10'] } } }
        vehicle:
          type: [object, 'null']
          properties:
            latitude: { type: number, examples: [53.8201] }
            longitude: { type: number, examples: [-1.5794] }
            updated_at: { type: string, format: date-time }
        driver_first_name: { type: [string, 'null'], examples: [Jamal] }
        tracking_url: { type: string, format: uri }
    EventType:
      type: string
      enum: [consignment.created, consignment.scanned, consignment.booking_requested, consignment.booked, consignment.out_for_delivery, consignment.delivered, consignment.failed_attempt, consignment.cancelled, consignment.returned]
    Event:
      type: object
      properties:
        id: { type: string, examples: [evt_3Fq9] }
        type: { $ref: '#/components/schemas/EventType' }
        at: { type: string, format: date-time }
        summary: { type: string, examples: ['Scanned at Leeds depot (2 of 3 pieces)'] }
    ProofOfDelivery:
      type: object
      properties:
        consignment: { type: string }
        delivered_at: { type: string, format: date-time }
        signed_by: { type: string, examples: [S. Khan] }
        signature_url: { type: string, format: uri }
        photo_urls:
          type: array
          items: { type: string, format: uri }
        location: { type: object, properties: { latitude: { type: number }, longitude: { type: number }, accuracy_m: { type: number, examples: [8] } } }
        notes: { type: string, examples: [Taken to the back bedroom] }
    WebhookEndpoint:
      type: object
      properties:
        id: { type: string, examples: [we_4Tn1] }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: '#/components/schemas/EventType' }
        status: { type: string, enum: [enabled, disabled] }
        created_at: { type: string, format: date-time }
    WebhookEvent:
      type: object
      properties:
        id: { type: string, examples: [evt_3Fq9] }
        type: { $ref: '#/components/schemas/EventType' }
        created: { type: string, format: date-time }
        data: { $ref: '#/components/schemas/Consignment' }
    Account:
      type: object
      properties:
        id: { type: string, examples: [shp_Oak1] }
        name: { type: string, examples: [Oakly Home Ltd] }
        courier: { type: string, examples: [Leeds Couriers] }
        mode: { type: string, enum: [live, sandbox] }
