openapi: 3.1.0
info:
  title: PostalForm Projects API
  version: 2026-05-06
  description: Cloudflare-only PostalForm Projects API for Stripe Projects-style developer workspaces. Supports U.S./Canada routing, selected PinGen local-print international destinations, PinGen Fast for selected destinations, and provider fallback routing for eligible standard letters.
servers:
  - url: https://projects.postalform.com
paths:
  /docs:
    get:
      summary: View the hosted Projects API reference.
      responses:
        '200':
          description: HTML API reference page.
  /openapi.yaml:
    get:
      summary: Download this OpenAPI document.
      responses:
        '200':
          description: OpenAPI YAML.
          content:
            application/yaml:
              schema:
                type: string
  /openapi.public.yaml:
    get:
      summary: Download the public customer SDK OpenAPI document.
      responses:
        '200':
          description: Public OpenAPI YAML for customer SDK generation.
          content:
            application/yaml:
              schema:
                type: string
  /mock-projects/provider-account:
    post:
      summary: Create or reuse a mock Stripe Projects provider account.
      description: Used by `pf-projects` during development. Requests are signed with the mock Projects shared secret.
      security:
        - mockProjectsSignature: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MockProviderAccountRequest'
      responses:
        '200':
          description: Provider account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderAccountProvisionResponse'
  /mock-projects/resources:
    post:
      summary: Provision a mock Projects PostalForm workspace.
      security:
        - mockProjectsSignature: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MockResourceProvisionRequest'
      responses:
        '200':
          description: Workspace, credentials, dashboard claim token, and test credits.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MockResourceProvisionResponse'
  /mock-projects/resources/{resource_id}/billing:
    get:
      summary: Inspect mock resource billing state.
      security:
        - mockProjectsSignature: []
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Billing state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResourceBillingState'
    post:
      summary: Update mock resource billing rail.
      security:
        - mockProjectsSignature: []
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResourceUpgradeRequest'
      responses:
        '200':
          description: Updated billing state.
  /mock-projects/resources/{resource_id}/upgrade:
    post:
      summary: Upgrade a mock resource to prepaid credits or future SPT billing.
      security:
        - mockProjectsSignature: []
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResourceUpgradeRequest'
      responses:
        '200':
          description: Updated resource, payment contracts, and credentials.
  /mock-projects/resources/{resource_id}/rotate:
    post:
      summary: Rotate a mock resource API key.
      security:
        - mockProjectsSignature: []
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RotateApiKeyRequest'
      responses:
        '200':
          description: New API key secret. Returned once.
  /mock-projects/resources/{resource_id}:
    delete:
      summary: Disable a mock Projects workspace.
      security:
        - mockProjectsSignature: []
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Disabled workspace.
  /stripe-projects/catalog:
    get:
      summary: PostalForm service catalog exposed to Stripe Projects.
      security:
        - stripeProjectsSecret: []
      responses:
        '200':
          description: Service catalog.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeProjectsCatalog'
  /stripe-projects/status:
    get:
      summary: Internal Stripe Projects adapter status.
      security:
        - stripeProjectsSecret: []
      responses:
        '200':
          description: Provider accounts and resources.
  /stripe-projects/provider-account:
    post:
      summary: Create or reuse a provider account for a Stripe account.
      security:
        - stripeProjectsSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StripeProviderAccountRequest'
      responses:
        '201':
          description: Provider account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderAccountProvisionResponse'
  /stripe-projects/resources:
    post:
      summary: Provision a PostalForm resource for a Stripe Project.
      security:
        - stripeProjectsSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StripeResourceProvisionRequest'
      responses:
        '201':
          description: Created resource, credentials, and environment variables.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StripeResourceProvisionResponse'
        '200':
          description: Existing active resource.
  /stripe-projects/resources/{resource_id}:
    get:
      summary: Retrieve a provisioned Stripe Projects resource and env var names.
      security:
        - stripeProjectsSecret: []
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Resource and environment variables.
    delete:
      summary: Remove a Stripe Projects resource.
      security:
        - stripeProjectsSecret: []
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: Disabled resource.
  /stripe-projects/resources/{resource_id}/rotate:
    post:
      summary: Rotate credentials for a Stripe Projects resource.
      security:
        - stripeProjectsSecret: []
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RotateApiKeyRequest'
      responses:
        '200':
          description: Rotated credentials and environment variables.
  /stripe-projects/resources/{resource_id}/upgrade:
    post:
      summary: Upgrade a Stripe Projects resource to prepaid credits or SPT.
      security:
        - stripeProjectsSecret: []
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResourceUpgradeRequest'
      responses:
        '200':
          description: Updated resource and credentials.
  /api/v1/documents/upload-intent:
    post:
      summary: Create a PDF upload intent.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content_type:
                  type: string
                  const: application/pdf
                byte_size:
                  type: integer
                  minimum: 1
                page_count:
                  type: integer
                  minimum: 1
                  description: Optional known PDF page count. Used for routing and pricing if supplied.
              required: [content_type, byte_size]
      responses:
        '200':
          description: Upload instructions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadIntent'
  /api/v1/documents/{document_id}/complete:
    post:
      summary: Mark an uploaded document complete after the object is present.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/DocumentId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                page_count:
                  type: integer
                  minimum: 1
                  description: Optional PDF page count if it was not supplied during upload-intent creation.
      responses:
        '200':
          description: Validated document.
          content:
            application/json:
              schema:
                type: object
                properties:
                  document_id:
                    type: string
                  status:
                    type: string
                    enum: [validated]
  /api/v1/letters/quotes:
    post:
      summary: Quote a mailpiece.
      description: Quote a letter and choose provider routing from document size, country codes, mail class, and proof-mail settings. Country codes default to US when omitted. U.S./Canada standard mail prefers Lob when eligible, U.S./Canada Express routes to Click2Mail, supported international local-print destinations can route to PinGen, and selected PinGen destinations support Express via PinGen Fast. Long or ineligible standard pieces fall back to PostGrid where supported.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateQuoteRequest'
      responses:
        '200':
          description: Quote.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quote'
  /api/v1/letters:
    post:
      summary: Create a test or live mail order from a quote.
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateLetterRequest'
      responses:
        '201':
          description: Created order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Letter'
        '200':
          description: Idempotent replay.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Letter'
  /api/v1/letters/{order_id}:
    get:
      summary: Retrieve a mail order, provider submissions, timeline, and customer webhook events.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrderId'
      responses:
        '200':
          description: Order detail.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Letter'
                  - type: object
                    properties:
                      timeline:
                        type: array
                        items:
                          $ref: '#/components/schemas/MailOrderEvent'
                      provider_submissions:
                        type: array
                        items:
                          $ref: '#/components/schemas/ProviderSubmission'
                      webhook_events:
                        type: array
                        items:
                          $ref: '#/components/schemas/WebhookEvent'
  /api/v1/letters/{order_id}/document.pdf:
    get:
      summary: Preview or download the PDF for a letter order.
      description: Streams the prepared PDF when available, otherwise the original uploaded PDF while preparation is pending. Documents follow the workspace document retention window.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrderId'
        - name: version
          in: query
          required: false
          schema:
            type: string
            enum: [current, original, prepared]
            default: current
          description: current returns the prepared PDF when available and otherwise the original upload.
        - name: disposition
          in: query
          required: false
          schema:
            type: string
            enum: [inline, attachment]
            default: inline
          description: Use attachment to download instead of previewing inline.
      responses:
        '200':
          description: PDF bytes.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
  /api/v1/postcards/quotes:
    post:
      summary: Quote a postcard mailpiece.
      description: Postcards require a fully composed two-page PDF and a postcard_size of 4x6, 6x9, or 11x6. Postcards force first-class color double-sided mail, do not support certified or registered mail, and do not route to Click2Mail. Lob-routed international postcards require postcard_size=4x6 and a U.S. sender return address; larger international postcards or non-U.S. return addresses route to PostGrid where available.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePostcardQuoteRequest'
      responses:
        '200':
          description: Postcard quote.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Quote'
  /api/v1/postcards:
    post:
      summary: Create a test or live postcard order from a postcard quote.
      security:
        - bearerAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateLetterRequest'
      responses:
        '201':
          description: Created postcard order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Letter'
        '200':
          description: Idempotent replay.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Letter'
  /api/v1/postcards/{order_id}:
    get:
      summary: Retrieve a postcard order, provider submissions, timeline, and customer webhook events.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrderId'
      responses:
        '200':
          description: Postcard order detail.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Letter'
                  - type: object
                    properties:
                      timeline:
                        type: array
                        items:
                          $ref: '#/components/schemas/MailOrderEvent'
                      provider_submissions:
                        type: array
                        items:
                          $ref: '#/components/schemas/ProviderSubmission'
                      webhook_events:
                        type: array
                        items:
                          $ref: '#/components/schemas/WebhookEvent'
  /api/v1/postcards/{order_id}/document.pdf:
    get:
      summary: Preview or download the PDF for a postcard order.
      description: Streams the prepared postcard PDF when available, otherwise the original uploaded PDF while preparation is pending. Documents follow the workspace document retention window.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/OrderId'
        - name: version
          in: query
          required: false
          schema:
            type: string
            enum: [current, original, prepared]
            default: current
          description: current returns the prepared PDF when available and otherwise the original upload.
        - name: disposition
          in: query
          required: false
          schema:
            type: string
            enum: [inline, attachment]
            default: inline
          description: Use attachment to download instead of previewing inline.
      responses:
        '200':
          description: PDF bytes.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
  /api/v1/webhook-endpoints:
    get:
      summary: List customer webhook endpoints for the workspace.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Endpoints.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEndpoint'
    post:
      summary: Configure a customer webhook endpoint for status events.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  format: uri
                  pattern: '^https://'
                  description: HTTPS endpoint that receives signed PostalForm status webhooks.
                signing_secret:
                  type: string
                  description: Optional. Generated if omitted.
              required: [url]
      responses:
        '200':
          description: Created endpoint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpointSecretResponse'
  /api/v1/webhook-endpoints/{endpoint_id}:
    delete:
      summary: Disable a customer webhook endpoint.
      security:
        - bearerAuth: []
      parameters:
        - name: endpoint_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Disabled endpoint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
  /api/v1/webhook-endpoints/{endpoint_id}/rotate-secret:
    post:
      summary: Rotate a customer webhook endpoint signing secret.
      description: Returns the new signing secret once. Store it securely; list endpoints does not expose secrets.
      security:
        - bearerAuth: []
      parameters:
        - name: endpoint_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Rotated endpoint signing secret.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpointSecretResponse'
  /api/v1/webhook-events:
    get:
      summary: List customer webhook events for the workspace.
      description: Customer webhook events are provider status-change events only. Event names use `postalform.{letter|postcard}.{status}` and are emitted for `accepted_by_provider`, `in_transit`, `delivered`, `returned`, `failed`, and `canceled`.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Events.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEvent'
  /api/v1/webhook-events/{event_id}/replay:
    post:
      summary: Queue a customer webhook event for replay.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/EventId'
      responses:
        '200':
          description: Replay queued.
  /api/v1/credits/balance:
    get:
      summary: Get prepaid credit balance.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Balance.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditBalance'
  /api/v1/credits/payment-methods:
    get:
      summary: List saved live billing payment methods.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Saved payment methods for live auto-refill.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/BillingPaymentMethod'
  /api/v1/credits/payment-methods/setup-session:
    post:
      summary: Create a Checkout setup session for live credit auto-refill.
      description: Requires a live API key. The returned Checkout URL lets the customer save a payment method for future off-session auto-refill charges.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Checkout setup session URL.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentMethodSetupSession'
  /api/v1/credits/auto-refill:
    get:
      summary: Get credit auto-refill threshold policies.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Auto-refill policies for test and live credit balances.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditAutoRefillPolicies'
    post:
      summary: Configure the credit auto-refill threshold for the authenticated key mode.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreditAutoRefillRequest'
      responses:
        '200':
          description: Updated auto-refill policy.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CreditAutoRefillPolicy'
  /api/v1/credits/ledger:
    get:
      summary: List prepaid credit ledger entries.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Ledger entries.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CreditLedgerEntry'
  /api/v1/api-keys:
    get:
      summary: List API key prefixes and status.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: API keys.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiKey'
  /api/v1/api-keys/rotate:
    post:
      summary: Rotate a test or live API key.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                mode:
                  $ref: '#/components/schemas/Mode'
              required: [mode]
      responses:
        '200':
          description: New API key secret. Returned once.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyRotation'
  /api/v1/credits/checkout-session:
    post:
      summary: Create a prepaid credit top-up checkout session.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                amount_cents:
                  type: integer
                  minimum: 1
      responses:
        '200':
          description: Stripe Checkout URL in configured environments, or a local checkout stub URL for local development.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditCheckoutSession'
  /stripe/webhooks:
    post:
      summary: Receive Stripe Checkout and PaymentIntent events for credits.
      description: Handles prepaid top-up Checkout completions, payment-method setup completions, and auto-refill PaymentIntent success/failure events.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Event accepted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
        '400':
          description: Invalid Stripe signature or top-up metadata.
  /providers/{provider}/webhooks:
    post:
      summary: Receive provider status webhooks for Projects provider credentials.
      description: Lob and PostGrid events are resolved by provider plus provider letter ID. Unknown IDs are stored.
      parameters:
        - name: provider
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/Provider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Provider event resolution result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderEventResolution'
  /internal/dashboard/login-links:
    post:
      summary: Issue a short-lived dashboard magic login link.
      description: Internal/staging helper until outbound email delivery is wired for dashboard login.
      security:
        - internalSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
              required: [email]
      responses:
        '200':
          description: Magic login link.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DashboardLoginLink'
        '404':
          description: No dashboard user or owned workspace for this email.
  /internal/queues/drain:
    post:
      summary: Drain local/staging Projects queues.
      security:
        - internalSecret: []
      responses:
        '200':
          description: Processed queue counts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueueDrainResult'
  /internal/retention/cleanup:
    post:
      summary: Run retention cleanup for expired documents and disabled resources.
      security:
        - internalSecret: []
      responses:
        '200':
          description: Cleanup result.
  /internal/reconciliation/submission-pending:
    post:
      summary: Mark a provider submission as requiring manual reconciliation.
      security:
        - internalSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                order_id:
                  type: string
                provider:
                  type: string
              required: [order_id, provider]
      responses:
        '200':
          description: Reconciliation state.
  /internal/fulfillment/events:
    post:
      summary: Receive fulfillment service events.
      security:
        - internalSecret: []
      responses:
        '200':
          description: Accepted.
  /internal/provider-events/resolve:
    post:
      summary: Resolve provider events from internal tooling.
      security:
        - internalSecret: []
      responses:
        '200':
          description: Accepted.
  /mpp/v1/letters:
    post:
      summary: Future MPP letter endpoint.
      description: Currently disabled for live settlement in the initial Projects launch.
      responses:
        '501':
          description: MPP is not enabled.
  /mpp/v1/postcards:
    post:
      summary: Future MPP postcard endpoint.
      description: Currently disabled for live settlement in the initial Projects launch.
      responses:
        '501':
          description: MPP is not enabled.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
    mockProjectsSignature:
      type: apiKey
      in: header
      name: X-PostalForm-Mock-Signature
    stripeProjectsSecret:
      type: apiKey
      in: header
      name: X-PostalForm-Stripe-Projects-Secret
    internalSecret:
      type: apiKey
      in: header
      name: X-PostalForm-Internal-Secret
  parameters:
    ResourceId:
      name: resource_id
      in: path
      required: true
      schema:
        type: string
    DocumentId:
      name: document_id
      in: path
      required: true
      schema:
        type: string
    OrderId:
      name: order_id
      in: path
      required: true
      schema:
        type: string
    EventId:
      name: event_id
      in: path
      required: true
      schema:
        type: string
  schemas:
    Mode:
      type: string
      enum: [test, live]
    Provider:
      type: string
      enum:
        [click2mail, lob, postgrid, pingen, mock_click2mail, mock_lob, mock_postgrid, mock_pingen]
      description: Provider selected for fulfillment. Test keys return mock_* providers; live keys use the real provider names.
    MailingAddress:
      type: object
      description: Flexible mailing address object. Pass countryCode, country_code, country, address_country, or addressCountry for international destinations; country defaults to US when omitted.
      additionalProperties: true
      properties:
        name:
          type: string
        company:
          type: string
        line1:
          type: string
          description: Also accepts street1, address_line1, or addressLine1.
        line2:
          type: string
          description: Also accepts street2, address_line2, or addressLine2.
        city:
          type: string
          description: Also accepts address_city or addressCity.
        state:
          type: string
          description: Also accepts province, provinceOrState, address_state, or addressState.
        postal_code:
          type: string
          description: Also accepts postalCode, postalOrZip, zip, address_zip, or addressZip.
        countryCode:
          type: string
          minLength: 2
          maxLength: 2
          default: US
          description: ISO 3166-1 alpha-2 country code. Defaults to US when omitted.
    BillingTier:
      type: string
      enum: [test, prepaid_credits, future_projects_spt]
    MailOrderStatus:
      type: string
      enum:
        - queued
        - document_preparing
        - document_prepared
        - submitted_to_provider
        - accepted_by_provider
        - in_transit
        - delivered
        - returned
        - submission_pending
        - failed
        - canceled
      description: Order timeline status. `submission_pending` means the provider-submit job entered a physical-mail safety window where PostalForm cannot blindly retry without risking duplicate mail; it requires reconciliation if it remains the current order status.
    CustomerWebhookEventType:
      type: string
      description: Customer webhook event names emitted for provider status changes. Replace `letter` with `postcard` for postcard mailpieces.
      enum:
        - postalform.letter.accepted_by_provider
        - postalform.letter.in_transit
        - postalform.letter.delivered
        - postalform.letter.returned
        - postalform.letter.failed
        - postalform.letter.canceled
        - postalform.postcard.accepted_by_provider
        - postalform.postcard.in_transit
        - postalform.postcard.delivered
        - postalform.postcard.returned
        - postalform.postcard.failed
        - postalform.postcard.canceled
    CustomerWebhookPayload:
      type: object
      description: JSON body POSTed to customer webhook endpoints.
      properties:
        id:
          type: string
          example: evt_123
        type:
          $ref: '#/components/schemas/CustomerWebhookEventType'
        data:
          type: object
          properties:
            object:
              $ref: '#/components/schemas/Letter'
        provider:
          type: object
          properties:
            name:
              $ref: '#/components/schemas/Provider'
            provider_letter_id:
              type: string
            status_raw:
              type: string
              nullable: true
            status_normalized:
              type: string
              nullable: true
            imb_status:
              type: string
              nullable: true
            tracking_number:
              type: string
              nullable: true
            tracking_status:
              type: string
              nullable: true
    UploadIntent:
      type: object
      properties:
        document_id:
          type: string
        upload_url:
          type: string
          format: uri
        upload_method:
          type: string
          enum: [PUT]
        expires_at:
          type: string
          format: date-time
    CreateQuoteRequest:
      type: object
      description: Letter quote request. Provider selection is automatic; provider_strategy is not supported on public API requests. PostGrid fallback quotes from Canada to the Netherlands include a sheet-count postage guard when PinGen cannot accept the letter.
      properties:
        document_id:
          type: string
        page_count:
          type: integer
          minimum: 1
          description: Optional explicit PDF page count. Used for provider routing and pricing when present.
        mail_class:
          type: string
          default: usps_first_class
          description: Standard/USPS First Class by default. Accepts standard/usps_first_class, priority/usps_priority, and express/usps_express. Priority/Express routes through Click2Mail for U.S./Canada destinations and through PinGen Fast for eligible AT, CH, DE, GB, IN, and NL local-print destinations. Priority/Express cannot be combined with certified or registered proof mail.
        color:
          type: boolean
          default: false
        double_sided:
          type: boolean
          default: true
        certified:
          type: boolean
          default: false
          description: Proof-mail add-on for letters only. Uses USPS Certified Mail for eligible U.S. standard letters and PinGen registered mail for eligible Belgium, Switzerland, and France standard letters.
        certified_return_receipt:
          type: boolean
          default: false
          description: Electronic return receipt for eligible U.S. Certified Mail letters.
        signature_required:
          type: boolean
          default: false
          description: Signature confirmation for eligible U.S. Priority and Express letters.
        destination_country_code:
          type: string
          minLength: 2
          maxLength: 2
          default: US
          description: ISO 3166-1 alpha-2 destination country code used for provider routing. Defaults to US. First-party Projects destinations are US, CA, AT, BE, CH, DE, FR, GB, IN, LU, and NL. Other destinations may fall back to PostGrid where available.
        origin_country_code:
          type: string
          minLength: 2
          maxLength: 2
          default: US
          description: ISO 3166-1 alpha-2 sender/origin country code used for provider routing. Defaults to US.
      required: [document_id]
    CreatePostcardQuoteRequest:
      type: object
      description: Postcard quote request. Postcards force first-class color double-sided mail and cannot use certified or registered mail. Lob-routed international postcards require postcard_size=4x6 and origin_country_code=US; larger international postcards or non-U.S. return addresses route to PostGrid where available.
      properties:
        document_id:
          type: string
        page_count:
          type: integer
          minimum: 1
          description: Optional explicit PDF page count. Postcard source PDFs should be fully composed.
        postcard_size:
          type: string
          enum: [4x6, 6x9, 11x6]
        destination_country_code:
          type: string
          minLength: 2
          maxLength: 2
          default: US
          description: ISO 3166-1 alpha-2 destination country code used for provider routing. Defaults to US.
        origin_country_code:
          type: string
          minLength: 2
          maxLength: 2
          default: US
          description: ISO 3166-1 alpha-2 sender/origin country code used for provider routing. Defaults to US.
      required: [document_id, postcard_size]
    Quote:
      type: object
      properties:
        quote_id:
          type: string
        mailpiece_type:
          type: string
          enum: [letter, postcard]
        postcard_size:
          type:
            - string
            - 'null'
          enum: [4x6, 6x9, 11x6, null]
        price_cents:
          type: integer
        currency:
          type: string
          enum: [usd]
        pricing_version:
          type: string
        certified_return_receipt:
          type: boolean
        signature_required:
          type: boolean
        expires_at:
          type: string
          format: date-time
    CreateLetterRequest:
      type: object
      description: Create an order from a quote. Requires Idempotency-Key header. Sender is optional for some providers but required for PinGen-routed international mail; include sender for all international sends.
      properties:
        quote_id:
          type: string
        recipient:
          $ref: '#/components/schemas/MailingAddress'
        sender:
          $ref: '#/components/schemas/MailingAddress'
        metadata:
          type: object
          description: Optional caller metadata stored on the order and surfaced in reads/webhook payloads.
          additionalProperties: true
      required: [quote_id, recipient]
    Letter:
      type: object
      properties:
        id:
          type: string
        object:
          type: string
          enum: [letter, postcard]
        mailpiece_type:
          type: string
          enum: [letter, postcard]
        postcard_size:
          type:
            - string
            - 'null'
          enum: [4x6, 6x9, 11x6, null]
        status:
          $ref: '#/components/schemas/MailOrderStatus'
        mode:
          $ref: '#/components/schemas/Mode'
        price_cents:
          type: integer
        currency:
          type: string
        funds_status:
          type: string
        billing_rail:
          type: string
        selected_provider:
          type: string
          description: Provider chosen by quote routing, using mock_* provider names for test keys.
        idempotent_replay:
          type: boolean
    ProviderSubmission:
      type: object
      properties:
        id:
          type: string
        provider:
          type: string
        provider_letter_id:
          type: string
        provider_status:
          type: string
        provider_status_normalized:
          type: string
        tracking_number:
          type: string
        raw_response:
          type: object
          additionalProperties: true
    MailOrderEvent:
      type: object
      properties:
        id:
          type: string
        event_type:
          type: string
        status_before:
          type: string
        status_after:
          type: string
        source:
          type: string
        provider:
          type: string
        provider_letter_id:
          type: string
        created_at:
          type: string
          format: date-time
    WebhookEndpoint:
      type: object
      properties:
        id:
          type: string
        url:
          type: string
          format: uri
          pattern: '^https://'
        status:
          type: string
        created_at:
          type: string
          format: date-time
    WebhookEndpointSecretResponse:
      type: object
      properties:
        id:
          type: string
        url:
          type: string
          format: uri
          pattern: '^https://'
        status:
          type: string
        created_at:
          type: string
          format: date-time
        signing_secret:
          type: string
          description: Returned only when the endpoint is created or its signing secret is rotated.
    WebhookEvent:
      type: object
      properties:
        id:
          type: string
        workspaceId:
          type: string
        orderId:
          type: string
        eventType:
          $ref: '#/components/schemas/CustomerWebhookEventType'
        payload:
          $ref: '#/components/schemas/CustomerWebhookPayload'
        createdAt:
          type: string
          format: date-time
        deliveries:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDeliveryAttempt'
    WebhookDeliveryAttempt:
      type: object
      properties:
        id:
          type: string
        webhookEventId:
          type: string
        endpointId:
          type: string
        attemptNumber:
          type: integer
        status:
          type: string
          enum: [succeeded, failed]
        httpStatus:
          type: integer
        responseBodySnippet:
          type: string
        attemptedAt:
          type: string
          format: date-time
        nextRetryAt:
          type: string
          format: date-time
    CreditBalance:
      type: object
      properties:
        mode:
          type: string
          enum: [test, live]
        billing_rail:
          type: string
          enum:
            [
              mock_test_credits,
              prepaid_credits,
              future_projects_spt,
              future_metronome_streaming,
              future_mpp_tempo,
              manual_invoice,
            ]
        available_cents:
          type: integer
        reserved_cents:
          type: integer
        currency:
          type: string
        status:
          type: string
        test:
          $ref: '#/components/schemas/CreditBalanceSnapshot'
        live:
          $ref: '#/components/schemas/CreditBalanceSnapshot'
        auto_refill:
          $ref: '#/components/schemas/CreditAutoRefillPolicies'
    CreditBalanceSnapshot:
      type: object
      properties:
        available_cents:
          type: integer
        reserved_cents:
          type: integer
        currency:
          type: string
        status:
          type: string
    CreditAutoRefillPolicies:
      type: object
      properties:
        data:
          type: object
          properties:
            test:
              anyOf:
                - $ref: '#/components/schemas/CreditAutoRefillPolicy'
                - type: 'null'
            live:
              anyOf:
                - $ref: '#/components/schemas/CreditAutoRefillPolicy'
                - type: 'null'
        test:
          anyOf:
            - $ref: '#/components/schemas/CreditAutoRefillPolicy'
            - type: 'null'
        live:
          anyOf:
            - $ref: '#/components/schemas/CreditAutoRefillPolicy'
            - type: 'null'
    CreditAutoRefillRequest:
      type: object
      properties:
        mode:
          $ref: '#/components/schemas/Mode'
        enabled:
          type: boolean
          default: true
        threshold_cents:
          type: integer
          minimum: 100
          maximum: 100000
        refill_amount_cents:
          type: integer
          minimum: 100
          maximum: 100000
      required: [threshold_cents, refill_amount_cents]
    CreditAutoRefillPolicy:
      type: object
      properties:
        id:
          type: string
        workspace_id:
          type: string
        mode:
          $ref: '#/components/schemas/Mode'
        billing_rail:
          type: string
          enum: [mock_test_credits, prepaid_credits]
        enabled:
          type: boolean
        threshold_cents:
          type: integer
        refill_amount_cents:
          type: integer
        provider:
          type: string
          enum: [test_grant, stripe_checkout, local_checkout_stub]
        status:
          type: string
          enum: [active, disabled, payment_action_required, payment_pending, payment_failed]
        last_triggered_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    BillingPaymentMethod:
      type: object
      properties:
        id:
          type: string
        provider:
          type: string
          enum: [stripe]
        type:
          type: string
        brand:
          type: string
        last4:
          type: string
        exp_month:
          type: integer
        exp_year:
          type: integer
        status:
          type: string
          enum: [active, disabled]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
    PaymentMethodSetupSession:
      type: object
      properties:
        mode:
          type: string
          enum: [stripe_checkout, local_checkout_stub]
        checkout_url:
          type: string
          format: uri
    CreditLedgerEntry:
      type: object
      properties:
        id:
          type: string
        workspace_id:
          type: string
        credit_account_id:
          type: string
        order_id:
          type: string
        mode:
          type: string
          enum: [test, live]
        billing_rail:
          type: string
          enum:
            [
              mock_test_credits,
              prepaid_credits,
              future_projects_spt,
              future_metronome_streaming,
              future_mpp_tempo,
              manual_invoice,
            ]
        type:
          type: string
          enum: [test_grant, topup, reserve, capture, release, refund, adjustment]
        amount_cents:
          type: integer
        currency:
          type: string
        external_reference:
          type: string
        metadata:
          type: object
          additionalProperties: true
        created_at:
          type: string
          format: date-time
    CreditCheckoutSession:
      type: object
      properties:
        id:
          type: string
          description: PostalForm top-up ID.
        mode:
          type: string
          enum: [stripe_checkout, local_checkout_stub]
        checkout_url:
          type: string
          format: uri
        amount_cents:
          type: integer
        currency:
          type: string
    ApiKey:
      type: object
      properties:
        id:
          type: string
        mode:
          $ref: '#/components/schemas/Mode'
        prefix:
          type: string
        status:
          type: string
          enum: [active, disabled, revoked]
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
    ApiKeyRotation:
      type: object
      properties:
        mode:
          $ref: '#/components/schemas/Mode'
        api_key:
          type: string
          description: Full API key secret. Returned once.
        api_key_prefix:
          type: string
    MockProviderAccountRequest:
      type: object
      properties:
        mock_account_id:
          type: string
        mock_user_email:
          type: string
          format: email
        account_name:
          type: string
    MockResourceProvisionRequest:
      type: object
      properties:
        mock_account_id:
          type: string
        mock_project_id:
          type: string
        mock_user_email:
          type: string
          format: email
        service:
          type: string
          const: postalform/mail
        environment:
          type: string
          enum: [test, staging]
      required: [service]
    MockResourceProvisionResponse:
      type: object
      properties:
        created:
          type: boolean
        provider_account_id:
          type: string
        organization_id:
          type: string
        resource_id:
          type: string
        workspace_id:
          type: string
        test_api_key:
          type: string
        live_api_key_prefix:
          type: string
        live_api_key_status:
          type: string
        webhook_secret:
          type: string
        dashboard_claim_token:
          type: string
        dashboard_claim_url:
          type: string
          format: uri
        dashboard_url:
          type: string
          format: uri
        api_base:
          type: string
          format: uri
        credit_account_id:
          type: string
        mode:
          $ref: '#/components/schemas/Mode'
    ProviderAccountProvisionResponse:
      type: object
      properties:
        provider_account_id:
          type: string
        organization_id:
          type: string
        status:
          type: string
        reusable:
          type: boolean
        provider_account:
          $ref: '#/components/schemas/ProviderAccount'
    ProviderAccount:
      type: object
      properties:
        id:
          type: string
        organization_id:
          type: string
        source:
          type: string
        external_account_id:
          type: string
        owner_email:
          type: string
        status:
          type: string
    StripeProjectsCatalog:
      type: object
      properties:
        provider:
          type: string
          const: postalform
        services:
          type: array
          items:
            type: object
            properties:
              slug:
                type: string
                const: postalform/mail
              name:
                type: string
              categories:
                type: array
                items:
                  type: string
              tiers:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      $ref: '#/components/schemas/BillingTier'
                    name:
                      type: string
                    live_mail_enabled:
                      type: boolean
    StripeProviderAccountRequest:
      type: object
      properties:
        stripe_account_id:
          type: string
        stripe_provider_account_id:
          type: string
        owner_email:
          type: string
          format: email
        account_name:
          type: string
      required: [stripe_account_id]
    StripeResourceProvisionRequest:
      type: object
      properties:
        stripe_account_id:
          type: string
        stripe_project_id:
          type: string
        stripe_resource_id:
          type: string
        stripe_provider_account_id:
          type: string
        owner_email:
          type: string
          format: email
        project_name:
          type: string
        service:
          type: string
          const: postalform/mail
        tier:
          $ref: '#/components/schemas/BillingTier'
      required: [stripe_account_id, stripe_project_id, service]
    StripeResourceProvisionResponse:
      type: object
      properties:
        provider_account:
          $ref: '#/components/schemas/ProviderAccount'
        resource:
          $ref: '#/components/schemas/ProjectResource'
        workspace_id:
          type: string
        dashboard_url:
          type: string
          format: uri
        credentials:
          type: object
          additionalProperties:
            type: string
        environment_variables:
          type: array
          items:
            $ref: '#/components/schemas/EnvironmentVariable'
    ProjectResource:
      type: object
      properties:
        id:
          type: string
        workspace_id:
          type: string
        service:
          type: string
        source:
          type: string
        external_account_id:
          type: string
        external_project_id:
          type: string
        external_resource_id:
          type: string
        tier:
          $ref: '#/components/schemas/BillingTier'
        status:
          type: string
        health_status:
          type: string
    EnvironmentVariable:
      type: object
      properties:
        name:
          type: string
        value:
          type: string
        sensitive:
          type: boolean
    ResourceUpgradeRequest:
      type: object
      properties:
        tier:
          $ref: '#/components/schemas/BillingTier'
        rail:
          $ref: '#/components/schemas/BillingTier'
        shared_payment_token:
          type: string
        stripe_customer_id:
          type: string
        currency:
          type: string
          default: usd
        max_amount_cents:
          type: integer
        expires_at:
          type: string
          format: date-time
    RotateApiKeyRequest:
      type: object
      properties:
        mode:
          $ref: '#/components/schemas/Mode'
      required: [mode]
    ResourceBillingState:
      type: object
      properties:
        workspace_id:
          type: string
        billing_mode:
          type: string
        live_enabled:
          type: boolean
        payment_contracts:
          type: array
          items:
            $ref: '#/components/schemas/PaymentContract'
        credit_balance:
          $ref: '#/components/schemas/CreditBalance'
    PaymentContract:
      type: object
      properties:
        id:
          type: string
        rail:
          type: string
        stripe_customer_id:
          type: string
        max_amount_cents:
          type: integer
        currency:
          type: string
        expires_at:
          type: string
          format: date-time
        status:
          type: string
    ProviderEventResolution:
      type: object
      properties:
        status:
          type: string
          enum: [resolved, unknown]
        provider:
          $ref: '#/components/schemas/Provider'
        provider_letter_id:
          type: string
        order_id:
          type: string
        submission_id:
          type: string
        provider_event_id:
          type: string
    DashboardLoginLink:
      type: object
      properties:
        email:
          type: string
          format: email
        login_url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time
        workspace_ids:
          type: array
          items:
            type: string
    QueueDrainResult:
      type: object
      properties:
        document_prepare_processed:
          type: integer
        provider_submit_processed:
          type: integer
        outbound_webhook_processed:
          type: integer
