> ## Documentation Index
> Fetch the complete documentation index at: https://docs.truscan.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Payment provider webhook (Standard Webhooks)

> Public by necessity: the payment provider holds no session and cannot present a JWT, so this route cannot sit behind the edge's identity middleware. Authenticity comes from the HMAC-SHA256 signature instead. Unsigned or mis-signed deliveries are refused before anything is read, the body is verified as the exact bytes received, and every delivery is deduped by webhook-id before any side effect. NOT enveloped, because the provider reads the status line.



## OpenAPI

````yaml /api-reference/billing.json post /api/billing/webhook
openapi: 3.1.0
info:
  title: truscan billing
  version: 1.0.0
  description: >-
    Credits, metering and top-ups. Money is counted in MICRO-DOLLARS (millionths
    of a dollar) as integers everywhere, so $7.00 per 1,000 requests is exactly
    7,000 micros per request. Floats drift, and a billing system that drifts
    gets disputed. Every amount is accompanied by a pre-formatted display string
    so no client reimplements money formatting. All responses use the {success,
    result, message} envelope except health and the webhook.
servers:
  - url: https://api.truscan.co
    description: Production
security: []
paths:
  /api/billing/webhook:
    post:
      summary: Payment provider webhook (Standard Webhooks)
      description: >-
        Public by necessity: the payment provider holds no session and cannot
        present a JWT, so this route cannot sit behind the edge's identity
        middleware. Authenticity comes from the HMAC-SHA256 signature instead.
        Unsigned or mis-signed deliveries are refused before anything is read,
        the body is verified as the exact bytes received, and every delivery is
        deduped by webhook-id before any side effect. NOT enveloped, because the
        provider reads the status line.
      parameters:
        - name: webhook-id
          in: header
          required: true
          schema:
            type: string
        - name: webhook-timestamp
          in: header
          required: true
          schema:
            type: string
        - name: webhook-signature
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProviderEvent'
      responses:
        '200':
          description: Accepted, or a replay that was already applied
        '400':
          description: Unreadable body, or a timestamp outside the five-minute tolerance
        '401':
          description: Signature does not match
        '500':
          description: Transient failure. The provider retries on its backoff schedule.
        '503':
          description: >-
            No webhook secret configured. Fails closed rather than trusting an
            unsigned payload.
components:
  schemas:
    ProviderEvent:
      type: object
      properties:
        business_id:
          type: string
        type:
          type: string
          enum:
            - payment.succeeded
            - payment.failed
            - payment.cancelled
        timestamp:
          type: string
          format: date-time
        data:
          type: object

````