# Guide to Paxos Webhooks: Features and Best Practices

Paxos webhooks tell your platform when something changes on the Paxos side: an identity is approved or needs documents, a crypto or fiat deposit completes or fails, a withdrawal goes out, or an orchestration moves through its steps. If you build on Paxos for stablecoins, crypto brokerage or tokenised assets, webhooks let you react to those changes without polling the Events API.

This guide covers how Paxos webhooks are configured and authenticated, why every payload is a thin notification you resolve through the API, the event families, Paxos's retry and rate limit rules, and the best practices for production.

## What are Paxos webhooks?

A Paxos webhook sends an HTTP `POST` to your endpoint, which Paxos calls a webhook consumer, whenever an event of a type you selected occurs. You create webhooks in the Paxos Dashboard, under the **Developer** tab.

The design choice that shapes everything else is that Paxos doesn't put the event's details in the webhook. Each delivery carries only an event ID, its type, a timestamp and two flags. To learn what actually happened, your consumer calls the authenticated Get Event API with that ID. Paxos explains why: it "does not currently support consumer-side signature-verification on webhook event messages", so to keep things secure it requires an authenticated request to fetch the full event. Paxos also says it is working on signature verification.

For authentication, a webhook uses one of two auth types. **API Key** sends a header whose name and value you choose. **OAuth** has Paxos fetch an access token from your own OAuth server with the client credentials grant.

## Paxos webhook features

| Feature | Details |
| --- | --- |
| Configuration | Paxos Dashboard, **Developer > Webhooks** |
| Limits | 10 active webhooks per entity, with unique names |
| Method and format | `POST` with a JSON body. The URL must be HTTPS |
| Payload | Thin: `id`, `type`, `source`, `time`, `object`, `is_test` |
| Full event | `GET /v2/events/{id}` (scope `events:read_event`) |
| Event identifier | `type` |
| Authentication | API Key (a header you name) or OAuth (client credentials). No signature |
| Delivery | At least once |
| Retries | Any non-`2xx`, with exponential backoff and jitter from about 5 seconds, for up to 24 hours |
| Rate limit | 1 to 100 requests per second per webhook, default 10 |
| Test events | Sent from the Dashboard with `is_test: true` |
| Approvals | Optional approval workflow for creating, updating and deleting webhooks |

## Common events

Paxos documents 70 event types in three families.

| Family | Examples | What they cover |
| --- | --- | --- |
| Identity | `identity.approved`, `identity.denied`, `identity.disabled`, `identity.documents_required`, `identity.documents_accepted`, `identity.documents_denied`, `identity.kyc_refresh.started`, `identity.kyc_refresh.completed`, `identity.kyc_refresh.expired` | KYC onboarding and periodic refresh for your end users |
| Transfer | `transfer.crypto_deposit.completed`, `transfer.crypto_withdrawal.failed`, `transfer.wire_deposit.pending`, `transfer.rtp_deposit.completed`, `transfer.internal_transfer_credit.completed` | Deposits and withdrawals on each rail (crypto, wire, RTP and several bank networks), each with `pending`, `completed` and `failed` |
| Orchestration | `orchestration.pending`, `orchestration.processing`, `orchestration.completed`, `orchestration.failed`, `orchestration.rejected`, `orchestration.status_details.conversion_failed` | Multi-step flows, with finer-grained `status_details` events |

Every payload has the same six fields. Here is Paxos's `identity.approved` example:

```json
{
  "id": "bd019f1c-89a7-4372-9d21-eaad9280dc41",
  "type": "identity.approved",
  "source": "com.paxos",
  "time": "2025-01-07T14:30:02Z",
  "object": "event",
  "is_test": false
}

```

`source` is always `com.paxos` and `object` is always `event`. `time` is RFC 3339. `id` is both your idempotency key and what you pass to the Get Event API to fetch the identity, transfer or orchestration the event is about.

## Setting up Paxos webhooks

1. Generate a long random value, for example with `openssl rand -hex 32`, and choose a header name such as `X-Paxos-Webhook-Key`.
2. In the Paxos Dashboard, open **Developer > Webhooks** and click **Create Webhook**.
3. Enter a unique name and your HTTPS URL, choose **API Key** as the auth type, and enter your header name and value.
4. Select the event types you want and set the rate limit. Click **Create**. If approvals are enabled, the webhook waits for approval first.
5. Use **Actions > Test** to send a test event, which arrives with `is_test: true`.

Paxos's quickstart has you test the consumer locally, then in sandbox, before production.

**In Hookdeck.** Create a source with the Paxos type, enter your header name in **Header Name** and the exact value in **Header Value**, and use the source URL as the webhook URL in Paxos. Hookdeck matches the header name case-insensitively and the value exactly. The Paxos source type works with the API Key auth type. With OAuth, the bearer token comes from your OAuth server and changes, so there's no fixed value to compare.

## Securing Paxos webhooks

Check the API Key header in constant time, then treat the body as a pointer: fetch the event from Paxos before acting on it.

```javascript
const crypto = require('crypto');
const express = require('express');

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on buffers of different lengths
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/paxos', express.raw({ type: '*/*' }), (req, res) => {
  const expected = process.env.PAXOS_WEBHOOK_SECRET;
  if (!expected) return res.sendStatus(500); // fail closed when unconfigured

  const received = req.get(process.env.PAXOS_WEBHOOK_HEADER || 'X-Paxos-Webhook-Key');
  if (typeof received !== 'string' || !safeEqual(received, expected)) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (event.is_test) return res.sendStatus(200);

  // Deduplicate on event.id, then queue a job that calls
  // GET https://api.paxos.com/v2/events/{event.id} with your API credentials.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

def verify_paxos_request(headers) -> bool:
    expected = os.environ.get("PAXOS_WEBHOOK_SECRET")
    if not expected:
        return False  # fail closed
    header_name = os.environ.get("PAXOS_WEBHOOK_HEADER", "X-Paxos-Webhook-Key")
    received = headers.get(header_name)  # Starlette and Flask headers are case-insensitive
    if not received:
        return False
    return hmac.compare_digest(received.encode("utf-8"), expected.encode("utf-8"))

```

The API Key header proves the caller knows the value, and nothing more: it doesn't protect the body, and a captured request can be replayed. Paxos's thin payload limits the damage. A forged notification can only point at an event ID, and if that ID doesn't exist in your account, Get Event won't return it. Never act on the `type` field alone. Keep the endpoint on HTTPS, use a long random value, and rotate it in the Dashboard and your configuration if it may have leaked.

> **Make Paxos webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the API Key header on every Paxos delivery, answers well inside Paxos's latency target, and queues each event durably with replay.

## Paxos webhook limitations and pain points

### Every event needs a second request

**The Problem:** Your consumer can't act on a webhook until it has called the Paxos API, so each event costs an extra request, and your consumer needs API credentials as well as the webhook secret.

**Why It Happens:** Paxos leaves event details out of the payload on purpose, because webhooks aren't signed. The Get Event API is the trusted source.

**Workarounds:**

* Acknowledge first, then fetch the event in a background job, so the API call never slows your response.
* Respect Paxos API rate limits when many events arrive at once, for example after an outage.

**How Hookdeck Can Help:** Hookdeck answers Paxos as soon as the delivery arrives, and can rate-limit delivery to your consumer, so a burst of events turns into a steady stream of Get Event calls instead of a spike.

### A static header, and signatures still to come

**The Problem:** The only check on a delivery is a fixed header value that can't detect changes or replays.

**Why It Happens:** Paxos doesn't sign webhooks yet. The API Key auth type sends the same value on every request.

**Workarounds:**

* Resolve every event through Get Event, and ignore IDs that don't exist in your account.
* Deduplicate on `id`.
* Watch Paxos's webhook docs. When signatures ship, add verification.

**How Hookdeck Can Help:** Hookdeck's Paxos source type compares the header you name with the exact value you enter. Hookdeck answers Paxos with a `200` either way, then records a request without the right header as failed verification, so it never reaches your destination.

### After 24 hours, events are gone from the webhook

**The Problem:** An outage longer than a day leaves events that will never be delivered by webhook.

**Why It Happens:** Paxos retries any non-`2xx` response with exponential backoff "for up to 24 hours from the initial attempt", and then stops.

**Workarounds:**

* Use the List Events API to find events from the gap, then process them through your normal handling, as Paxos recommends.
* Run a periodic backup poll of the Events API even when things are healthy.

**How Hookdeck Can Help:** Once a delivery has reached Hookdeck, it's stored and can be replayed whenever your consumer is ready, so your recovery doesn't depend on Paxos's 24-hour window.

### Delivery rate is capped per webhook

**The Problem:** During busy periods, events back up on the Paxos side and arrive late.

**Why It Happens:** Each webhook has a delivery rate limit of 1 to 100 requests per second, with a default of 10.

**Workarounds:**

* Raise the rate limit to what your consumer can handle.
* Split event families across webhooks if one family is much busier than the others, within the limit of 10 active webhooks.

**How Hookdeck Can Help:** Because Hookdeck acknowledges in milliseconds, you can raise Paxos's rate limit without worrying about your own service, and set a separate delivery rate on the Hookdeck side to match what your consumer can process.

## Best practices

### Ignore test events

Dashboard tests arrive with `is_test: true`. Check the flag before doing anything else, as Paxos's own examples do.

### Deduplicate on `id`

Delivery is at least once. Store each `id` before processing and skip repeats. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Paxos asks for a `2xx` "as quickly as possible (ideally < 500ms)". Verify the header, persist the ID, respond, and fetch the event afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Back up the webhook with polling

Paxos recommends a periodic job that polls the Events API as a fallback for missed webhook events. Run it, and compare what it finds with what your webhook handler processed.

### Route on `type` families

Branch first on the family (`identity.`, `transfer.`, `orchestration.`), then on the specific type. New transfer rails add new types, so acknowledge types you don't recognise rather than failing them.

## Conclusion

Paxos sends a thin notification for each event you subscribe to, authenticated by an API Key header you choose, and expects you to fetch the real event from its API. Compare the header in constant time over HTTPS, skip test events, deduplicate on `id`, respond within half a second, and keep a polling fallback, because retries stop after 24 hours.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your consumer, checks the API Key header, answers Paxos immediately, and keeps every notification until your service has resolved and processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Paxos webhooks reliably in minutes.