openapi: 3.0.3
info:
  title: Track API
  description: |
    Package tracking API. Auto-detects carriers, normalizes statuses across
    1,200+ carriers, pushes signed webhooks on updates.

    Billing counts trackings created per calendar month; reads and webhooks
    are free. Resubmitting an already-tracked number is idempotent and never
    billed twice.
  version: "2026-07"
servers:
  - url: https://track.example.com
security:
  - bearerAuth: []
paths:
  /v1/signup:
    post:
      summary: Instant signup — returns an API key in one call
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: {type: string, format: email}
      responses:
        "201":
          description: Account created; the key is shown only once.
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_key: {type: string, example: trk_live_a1B2c3...}
                  email: {type: string}
                  plan: {type: string, example: free}
        "200": {description: Account already exists; a login link was emailed.}
        "429": {$ref: "#/components/responses/RateLimited"}
  /v1/me:
    get:
      summary: Current account and usage
      responses:
        "200":
          description: Account info
          content:
            application/json:
              schema:
                type: object
                properties:
                  email: {type: string}
                  plan: {type: string}
                  usage:
                    type: object
                    properties:
                      used: {type: integer}
                      quota: {type: integer}
  /v1/trackings:
    post:
      summary: Start tracking a package
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tracking_number]
              properties:
                tracking_number: {type: string, example: 1Z12345E0205271688}
                carrier: {type: string, description: "Carrier code; omit to auto-detect. China carriers via kuaidi100:<com> (e.g. kuaidi100:shunfeng)", example: ups}
                external_order_id: {type: string}
                notify_email: {type: string, format: email, description: "Email the recipient on meaningful status changes (out-for-delivery, delivered, exception)."}
                metadata: {type: object, additionalProperties: {type: string}}
                extra:
                  type: object
                  additionalProperties: {type: string}
                  description: Carrier-specific params some carriers require (postal code, ship date).
      responses:
        "201":
          description: New tracking created (1 quota unit).
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Tracking"}
        "200":
          description: Already tracked — idempotent, not billed.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Tracking"}
        "422": {description: Invalid tracking number.}
        "429": {$ref: "#/components/responses/RateLimited"}
    get:
      summary: List trackings
      parameters:
        - {name: limit, in: query, schema: {type: integer, default: 50, maximum: 200}}
        - {name: offset, in: query, schema: {type: integer, default: 0}}
      responses:
        "200":
          description: Trackings, most recent first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: {$ref: "#/components/schemas/Tracking"}
  /v1/trackings/{id}:
    get:
      summary: Get one tracking with its full event timeline
      parameters: [{name: id, in: path, required: true, schema: {type: string, format: uuid}}]
      responses:
        "200":
          description: Tracking detail.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Tracking"}
        "404": {description: Not found.}
    delete:
      summary: Stop tracking (archive)
      parameters: [{name: id, in: path, required: true, schema: {type: string, format: uuid}}]
      responses:
        "204": {description: Archived.}
        "404": {description: Not found.}
  /v1/trackings/{id}/refresh:
    post:
      summary: Request an immediate re-poll (rate-clamped server-side)
      parameters: [{name: id, in: path, required: true, schema: {type: string, format: uuid}}]
      responses:
        "202": {description: Refresh scheduled.}
        "404": {description: Not found.}
  /v1/carriers:
    get:
      summary: List supported carriers
      security: []
      responses:
        "200":
          description: Carrier catalog.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        code: {type: string, example: usps}
                        name: {type: string, example: USPS}
                        kind: {type: string, enum: [direct, aggregator]}
  /v1/webhooks:
    get:
      summary: List webhook endpoints
      responses:
        "200": {description: Endpoints.}
    post:
      summary: Add a webhook endpoint
      description: |
        Requires a verified email. Deliveries are signed with
        `X-Track-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + "." + body)>`,
        retried up to 7 times over ~3 days, unordered, at-least-once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: {type: string, format: uri}
                events:
                  type: array
                  items: {type: string, example: tracking.delivered}
                  description: Filter; empty = all events.
      responses:
        "201": {description: Created. The signing secret is shown only once.}
        "403": {description: Email not verified.}
  /v1/webhooks/{id}:
    delete:
      summary: Delete a webhook endpoint
      parameters: [{name: id, in: path, required: true, schema: {type: string, format: uuid}}]
      responses:
        "204": {description: Deleted.}
        "404": {description: Not found.}
  /v1/billing/checkout:
    post:
      summary: Create a checkout session for a paid plan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plan]
              properties:
                plan: {type: string, enum: [pro, business]}
      responses:
        "200":
          description: Redirect the user to this URL.
          content:
            application/json:
              schema: {type: object, properties: {url: {type: string}}}
  /v1/billing/portal:
    post:
      summary: Create a billing-portal session
      responses:
        "200": {description: Portal URL.}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API key from /v1/signup: Authorization: Bearer trk_live_..."
  responses:
    RateLimited:
      description: Burst rate limit or monthly quota exceeded.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            message: {type: string}
            code:
              type: string
              enum: [bad_request, unauthorized, not_found, invalid_number,
                     quota_exceeded, rate_limited, email_unverified, internal]
    Tracking:
      type: object
      properties:
        id: {type: string, format: uuid}
        tracking_number: {type: string}
        carrier: {type: string}
        status:
          type: string
          enum: [pending, info_received, in_transit, out_for_delivery,
                 available_for_pickup, delivery_failure, delivered, exception, expired]
        substatus: {type: string, example: in_transit.customs_cleared}
        external_order_id: {type: string}
        last_event_at: {type: string, format: date-time}
        created_at: {type: string, format: date-time}
        events:
          type: array
          description: Present on GET /v1/trackings/{id}; newest first.
          items:
            type: object
            properties:
              occurred_at: {type: string, format: date-time}
              status: {type: string}
              substatus: {type: string}
              description: {type: string}
              location:
                type: object
                properties:
                  city: {type: string}
                  state: {type: string}
                  country: {type: string}
                  postal: {type: string}
