Gareth Wilson Gareth Wilson

Guide to Paxos Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationPaxos Dashboard, Developer > Webhooks
Limits10 active webhooks per entity, with unique names
Method and formatPOST with a JSON body. The URL must be HTTPS
PayloadThin: id, type, source, time, object, is_test
Full eventGET /v2/events/{id} (scope events:read_event)
Event identifiertype
AuthenticationAPI Key (a header you name) or OAuth (client credentials). No signature
DeliveryAt least once
RetriesAny non-2xx, with exponential backoff and jitter from about 5 seconds, for up to 24 hours
Rate limit1 to 100 requests per second per webhook, default 10
Test eventsSent from the Dashboard with is_test: true
ApprovalsOptional approval workflow for creating, updating and deleting webhooks

Common events

Paxos documents 70 event types in three families.

FamilyExamplesWhat they cover
Identityidentity.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.expiredKYC onboarding and periodic refresh for your end users
Transfertransfer.crypto_deposit.completed, transfer.crypto_withdrawal.failed, transfer.wire_deposit.pending, transfer.rtp_deposit.completed, transfer.internal_transfer_credit.completedDeposits and withdrawals on each rail (crypto, wire, RTP and several bank networks), each with pending, completed and failed
Orchestrationorchestration.pending, orchestration.processing, orchestration.completed, orchestration.failed, orchestration.rejected, orchestration.status_details.conversion_failedMulti-step flows, with finer-grained status_details events

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

{
  "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.

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:

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 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.

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.

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 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 for free and handle Paxos webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.