# Payment Webhooks

Payment webhooks let Bead send real-time payment status updates to your system. Instead of polling, your server receives an HTTP `POST` whenever a payment’s `statusCode` changes.

Payment webhook notifications are typically used together with the Payments and Reporting APIs.

### When to use payment webhooks

Use payment webhooks when you want to:

* update orders or invoices as soon as a payment completes
* react to `underpaid`, `overpaid`, `expired`, `invalid`, or `cancelled` outcomes
* trigger downstream workflows such as fulfilment, refunds, customer notifications, or support tickets
* track payments without polling `GET /Payments/tracking/{trackingId}`

### How payment webhooks work

1. You configure a default webhook URL for a terminal, or provide `webhookUrls` when creating a payment.
2. Bead delivers an HTTP `POST` when a payment status changes.
3. Your server verifies the webhook signature using the stored `signingSecret`.
4. Your system records the event and updates internal state.
5. If needed, your system confirms the latest state using `GET /Payments/tracking/{trackingId}`.

### Configure payment webhook delivery

Payment webhooks are configured per terminal.

#### Default terminal webhook

Use the terminal webhook endpoint to set the default payment webhook URL for a terminal:

`PUT /Terminals/{id}/webhook`

The webhook URL should be an HTTPS endpoint that you control.

A successful setup response includes:

* `url`
* `signingSecret`

Store `signingSecret` securely. You will use it to verify incoming payment webhooks.

#### Optional per-payment webhook URLs

When creating a payment with `POST /Payments/crypto`, you can also provide `webhookUrls` for payment-specific delivery in addition to the terminal’s default webhook.

Use this when you want a payment to notify a specific backend flow without changing the terminal’s default webhook configuration.

### Where to get the signing secret

The payment webhook signing secret is associated with the terminal webhook configuration and should be stored securely when the webhook is set.

Important notes:

* do not expose `signingSecret` in client-side code
* do not expect the incoming webhook payload to include the secret
* do not compare the incoming signature header directly to the secret itself

The correct verification pattern is:

1. configure the webhook
2. save the `signingSecret`
3. receive the webhook delivery
4. compute the expected signature from the raw request body using the stored secret
5. compare your computed value to the signature header Bead sent
6. only trust the event if they match

### Event delivery

For each payment update, Bead sends a `POST` request to your webhook URL.

General behavior:

* method: `POST`
* content type: `application/json`
* one event per payment status change
* retries occur if your endpoint does not return a successful `2xx` response

Your webhook handler should:

* log every webhook request for debugging and audit
* preserve the raw request body before JSON parsing
* preserve request headers
* verify the signature before processing the payload
* return a successful response quickly after safely persisting or queueing the event

### Current payment signature header

Current payment webhook deliveries include the signature in:

`x-webhook-signature: <signature>`

Your listener should use `x-webhook-signature` together with the stored `signingSecret` and the raw request body to verify authenticity.

### Verifying the webhook

Always verify the payment webhook before processing the JSON body.

At a high level:

1. capture the raw request body exactly as received
2. read the `x-webhook-signature` header
3. compute the expected HMAC using the stored `signingSecret` and the raw body
4. compare your computed value to the received signature
5. reject the request if verification fails

If your environment includes timestamp information as part of the signature format, validate that as well to reduce replay risk.

### Example payload

```json
{
  "trackingId": "d3594f0680964156b21fab60f8573bb4",
  "paymentCode": "HM9N44Z43VTW",
  "statusCode": "cancelled",
  "amounts": {
    "requested": {
      "inPaymentCurrency": {
        "amount": 1,
        "currency": {
          "id": 16,
          "code": "USDC_BASE",
          "name": "USDC Base",
          "symbol": "USDC"
        }
      },
      "inRequestedCurrency": {
        "amount": 1,
        "currency": {
          "id": 1,
          "code": "USD",
          "name": "USD",
          "symbol": "$"
        }
      }
    },
    "paid": {
      "inPaymentCurrency": {
        "amount": 0,
        "currency": {
          "id": 16,
          "code": "USDC_BASE",
          "name": "USDC Base",
          "symbol": "USDC"
        }
      }
    }
  },
  "reference": "ORDER123",
  "description": null,
  "receivedTime": "2026-03-30T13:39:29.948491+00:00",
  "terminalId": "69aae38b1f3fe8c6698663dc",
  "merchantId": "664c5e3b0517b0a8a6321c9a",
  "errorMessage": null
}
```

### Common payment status values

Payment webhook events may include status values such as:

* `created`
* `processing`
* `underpaid`
* `overpaid`
* `completed`
* `expired`
* `invalid`
* `cancelled`

Your system should treat `statusCode` as the primary event classifier.

### Key fields

#### `trackingId`

This is the primary identifier for the payment. Use it to:

* correlate the webhook to your internal payment or order record
* call `GET /Payments/tracking/{trackingId}`
* reconcile webhook delivery with reporting and support workflows

#### `paymentCode`

This is the hosted payment code associated with the checkout.

#### `statusCode`

This tells you the payment’s current lifecycle state.

#### `terminalId`

This identifies the terminal that produced the payment.

#### `merchantId`

This identifies the merchant associated with the payment.

#### `receivedTime`

This is the timestamp associated with the event payload.

### Recommended processing model

1. Receive the request.
2. Preserve the raw request body and headers.
3. Read `x-webhook-signature`.
4. Compute the expected signature using the stored `signingSecret`.
5. Compare the computed value to the header value.
6. Reject the request if verification fails.
7. Parse the JSON payload only after verification succeeds.
8. Persist the event or enqueue it for processing.
9. Update your internal order or invoice state based on `statusCode`.
10. Return a successful `2xx` response quickly.

### Idempotency and duplicate handling

Treat payment webhook delivery as at least once.

A good idempotent processing strategy is:

* primary key: `trackingId` + `statusCode`
* optionally also store the raw body hash or received timestamp for debugging

Your system should be able to safely ignore duplicate deliveries without creating duplicate business actions.

### Delivery mechanics

Recommended expectations:

* respond quickly, ideally in under one second when possible
* do heavy work asynchronously after the event is safely persisted or queued
* expect retries with backoff when your endpoint fails or times out
* rely on signature verification rather than source IP allowlisting unless Bead explicitly publishes and supports source IP controls for your environment

### Confirming status with the Tracking endpoint

If you need to confirm the latest state during support, reconciliation, or after a missed event, call the tracking endpoint using the payment’s `trackingId`.

#### Endpoint

`GET /Payments/tracking/{trackingId}`

#### Headers

* `X-Api-Key: {apiKey}`
* `Accept: application/json`

#### Notes

* `apiKey` is the secret credential
* `maskedApiKey` is not usable
* the header name must be exactly `X-Api-Key`

### Testing webhooks in Sandbox

1. Configure a webhook URL for your Sandbox terminal.
2. Create a Sandbox payment with `POST /Payments/crypto`.
3. Complete the payment on the hosted page.
4. Confirm your webhook received payment status updates.
5. Confirm your listener captures `x-webhook-signature`.
6. Verify the signature using the stored `signingSecret`.
7. Optionally confirm the final state using `GET /Payments/tracking/{trackingId}`.

### Troubleshooting

#### I am receiving the webhook but verification fails

Check:

* you are using the raw request body, not a reserialized JSON body
* you are using the stored `signingSecret`
* you are reading `x-webhook-signature`
* your comparison logic is checking a computed signature against the incoming signature, not comparing the secret directly to the header

#### Webhook not received

Check:

* the terminal has a webhook URL configured
* your endpoint is publicly reachable over HTTPS
* your endpoint returns a successful `2xx` response quickly
* your server logs show inbound requests and any verification or parsing failures

#### Receiving repeated webhook events

This usually means your endpoint is timing out or returning a non-`2xx` response.

Return success quickly after safely persisting or queueing the event, and make your processing idempotent.

#### 401 Unauthorized when checking status

Payments endpoints use `X-Api-Key`.

Confirm:

* the API key is present
* the API key is valid for the environment
* the header name is exactly `X-Api-Key`

#### 403 Forbidden when checking status

The API key is valid but not permitted for the payment context, or you are mixing environments.

Confirm you are using the correct environment base URL and the correct terminal API key.

### Next steps

* Review [Payment Statuses](https://developers.bead.xyz/payments/payment-statuses) for the full list of `statusCode` values
* Review [Create Payment](https://developers.bead.xyz/payments/create-payment) for `webhookUrls` support when you need per-payment delivery
* Review [Webhook Event Reference](https://developers.bead.xyz/reference-guide/operational-guides/webhook-event-reference) for lower-level signature and delivery guidance
* Use [Reporting](https://developers.bead.xyz/reporting) and [Settlement](https://developers.bead.xyz/settlement) for historical views and reconciliation
