# Source of truth for the PassMaker public API.
# A copy is served to users at /openapi.yaml from frontend/public/openapi.yaml
# (the frontend Docker build context can't reach this docs/ path). Keep the two
# in sync when editing — SEAM: no automated copy step yet.
openapi: 3.1.0
info:
  title: PassMaker Public API
  version: "1.0.0"
  description: |
    Generate Apple / Google Wallet passes programmatically from templates you
    design in PassMaker. Each pass spends credits exactly like the web app
    (1 credit standard, 5 hybrid).

    ## Authentication
    Send your API key as a bearer token:
    `Authorization: Bearer pm_live_...`. Create and manage keys in
    Dashboard → API Keys. Keys are shown once at creation.

    ## Errors
    Every error uses the envelope `{ "error": { "code": "...", "message": "..." } }`
    with an appropriate HTTP status.

    ## Rate limiting
    Per-key sliding window (default 60 req/min). Every response carries
    `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`.
    A `429` includes `Retry-After`.

    ## Idempotency
    Write endpoints accept an `Idempotency-Key` header. Replaying the same key
    with the same body returns the original response (no extra credits spent);
    the same key with a different body returns `409`.
servers:
  - url: https://passmaker.io/api/v1
    description: Production

security:
  - apiKey: []

tags:
  - name: Templates
  - name: Passes
  - name: Batches
  - name: Webhooks
  - name: Account

paths:
  /templates:
    get:
      tags: [Templates]
      summary: List active templates
      responses:
        "200":
          description: The caller's active templates.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Template" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /templates/{id}:
    get:
      tags: [Templates]
      summary: Get a template and its variable schema
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Template detail.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Template" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /passes:
    post:
      tags: [Passes]
      summary: Create a pass from a template
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PassCreateRequest" }
      responses:
        "201":
          description: Pass created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Pass" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "422": { $ref: "#/components/responses/InvalidVariables" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Passes]
      summary: List passes
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 25, maximum: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
        - { name: template_id, in: query, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Paginated passes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Pass" }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /passes/{id}:
    get:
      tags: [Passes]
      summary: Get a pass
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Pass detail.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Pass" }
        "404": { $ref: "#/components/responses/NotFound" }

  /batches:
    post:
      tags: [Batches]
      summary: Create up to 500 passes asynchronously
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BatchCreateRequest" }
      responses:
        "202":
          description: Batch accepted; poll GET /batches/{id}.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Batch" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    get:
      tags: [Batches]
      summary: List batches
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 25, maximum: 100 } }
      responses:
        "200":
          description: The caller's batches (most recent first).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Batch" }

  /batches/{id}:
    get:
      tags: [Batches]
      summary: Get batch status + per-item results
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: limit, in: query, schema: { type: integer, default: 100, maximum: 500 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: Batch detail + items.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Batch" }
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/BatchItem" }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhooks:
    post:
      tags: [Webhooks]
      summary: Create a webhook endpoint (scope webhooks:write). Returns the signing secret once.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookCreateRequest" }
      responses:
        "201":
          description: Endpoint created; `secret` is returned only here.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WebhookEndpoint" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      responses:
        "200":
          description: The caller's webhook endpoints (no secrets).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookEndpoint" }

  /webhooks/{id}:
    get:
      tags: [Webhooks]
      summary: Get a webhook endpoint
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Endpoint detail.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WebhookEndpoint" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Webhooks]
      summary: Update a webhook endpoint (url / events / description / active)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookCreateRequest" }
      responses:
        "200":
          description: Updated endpoint.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/WebhookEndpoint" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook endpoint (unsubscribe)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Deleted.
        "404": { $ref: "#/components/responses/NotFound" }

  /webhooks/events:
    get:
      tags: [Webhooks]
      summary: List event types, each with a sample payload
      responses:
        "200":
          description: Event types and sample payloads (for building integrations).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        type: { type: string }
                        sample: { type: object, additionalProperties: true }

  /webhooks/{id}/test:
    post:
      tags: [Webhooks]
      summary: Send a sample event to the endpoint (Zap-editor / deliverability test)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  description: Which event's sample to send; omit for a generic ping.
                  enum: [pass.created, pass.email_delivered, pass.email_bounced, batch.completed, batch.failed]
      responses:
        "200":
          description: Delivery attempted; the result reflects the endpoint's HTTP response.
        "404": { $ref: "#/components/responses/NotFound" }

  /account:
    get:
      tags: [Account]
      summary: Credits remaining, key scopes, and limits
      responses:
        "200":
          description: Account summary.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Account" }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "API key in the form pm_live_… (Authorization: Bearer pm_live_…)"

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 255 }
      description: Retry-safety key. Replaying it returns the original response.

  schemas:
    Variable:
      type: object
      properties:
        key: { type: string }
        label: { type: string }
        type: { type: string, enum: [string] }
        required: { type: boolean }
        default: { type: string }
    Template:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        pass_type: { type: string }
        is_hybrid: { type: boolean }
        wallet_type: { type: string, enum: [apple, google, both] }
        variables:
          type: array
          items: { $ref: "#/components/schemas/Variable" }
        created_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
    PassCreateRequest:
      type: object
      required: [template_id]
      properties:
        template_id: { type: string, format: uuid }
        variables:
          type: object
          additionalProperties: { type: string }
          example: { name: "Ada Lovelace", seat: "12B" }
        recipient:
          type: object
          properties:
            email: { type: string, format: email }
            consent:
              type: object
              description: Required for cold sends above trust Tier 0.
              properties:
                method: { type: string }
                timestamp: { type: string, format: date-time }
        delivery:
          type: string
          enum: [email, none]
          default: none
        idempotency: { type: string, description: "Alternative to the Idempotency-Key header." }
    Pass:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string }
        pass_type: { type: string }
        is_hybrid: { type: boolean }
        template_id: { type: string, format: uuid }
        credits_charged: { type: integer }
        credits_remaining: { type: integer }
        delivery: { type: string, enum: [email, none] }
        delivery_status:
          type: string
          description: Present when delivery=email.
          enum: [sent, send_failed, suppressed, cold_limit_reached, consent_required]
        delivery_mode: { type: string, enum: [direct, claim] }
        apple:
          type: object
          properties:
            download_url: { type: string, format: uri }
            expires_at: { type: string, format: date-time }
        google:
          type: object
          properties:
            save_url: { type: string, format: uri }
        hybrid_view_url: { type: string, format: uri }
    BatchCreateRequest:
      type: object
      required: [template_id, items]
      properties:
        template_id: { type: string, format: uuid }
        delivery: { type: string, enum: [email, none], default: none, description: "Default for items that omit it." }
        items:
          type: array
          maxItems: 500
          items:
            type: object
            properties:
              variables: { type: object, additionalProperties: { type: string } }
              recipient:
                type: object
                properties:
                  email: { type: string, format: email }
              delivery: { type: string, enum: [email, none] }
    Batch:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [pending, processing, completed, failed] }
        source: { type: string, enum: [api, csv] }
        total: { type: integer }
        processed: { type: integer }
        succeeded: { type: integer }
        failed: { type: integer }
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time }
    BatchItem:
      type: object
      properties:
        row_number: { type: integer }
        status: { type: string, enum: [pending, success, failed] }
        pass_id: { type: string, format: uuid }
        error: { type: string }
    Account:
      type: object
      properties:
        credits_remaining: { type: integer }
        key:
          type: object
          properties:
            name: { type: string }
            prefix: { type: string }
            scopes: { type: array, items: { type: string } }
        limits:
          type: object
          properties:
            rate_limit_per_min: { type: integer }
            daily_pass_limit: { type: integer }
    Pagination:
      type: object
      properties:
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
        has_more: { type: boolean }
    WebhookCreateRequest:
      type: object
      required: [url]
      properties:
        url: { type: string, format: uri, description: HTTPS endpoint to POST events to. }
        events:
          type: array
          description: Event types to subscribe to; empty = all.
          items:
            type: string
            enum: [pass.created, pass.email_delivered, pass.email_bounced, batch.completed, batch.failed]
        description: { type: string }
        active: { type: boolean }
    WebhookEndpoint:
      type: object
      properties:
        id: { type: string, format: uuid }
        url: { type: string, format: uri }
        events: { type: array, items: { type: string } }
        description: { type: string }
        active: { type: boolean }
        secret: { type: string, description: "Signing secret (whsec_…); returned only on create." }
        last_delivery_at: { type: string, format: date-time }
        last_delivery_status: { type: string }
        created_at: { type: string, format: date-time }
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }

  responses:
    BadRequest:
      description: Malformed request.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unauthorized:
      description: Missing or invalid API key.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Forbidden:
      description: The key lacks the required scope.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: Resource not found.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    InsufficientCredits:
      description: Not enough credits.
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/Error"
              - type: object
                properties:
                  credits_needed: { type: integer }
                  credits_remaining: { type: integer }
    IdempotencyConflict:
      description: Idempotency-Key reused with a different request, or still processing.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    InvalidVariables:
      description: Required variables missing or invalid.
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/Error"
              - type: object
                properties:
                  variables:
                    type: array
                    items:
                      type: object
                      properties:
                        key: { type: string }
                        message: { type: string }
    RateLimited:
      description: Rate limit exceeded.
      headers:
        Retry-After: { schema: { type: integer }, description: Seconds to wait. }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

webhooks:
  passCreated:
    post:
      summary: pass.created / pass.email_delivered / pass.email_bounced / batch.completed / batch.failed
      description: |
        PassMaker POSTs a signed JSON body to your configured endpoint(s).
        Verify the `X-PassMaker-Signature: t=<unix>,v1=<hmac>` header — the HMAC
        is SHA-256 over `<t>.<rawBody>` using your endpoint's signing secret.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                type: { type: string }
                created: { type: string, format: date-time }
                data: { type: object, additionalProperties: true }
      responses:
        "2XX": { description: Acknowledged. Non-2xx is retried with backoff. }
