# Paxos Webhooks

Paxos sends a JSON webhook to your HTTPS endpoint when something happens on the platform: an identity is approved, a stablecoin transfer completes, an orchestration (mint, convert, redeem) finishes. Webhooks are **not signed** (Paxos says signature verification is planned). Instead you set **Auth Type: API Key** on the webhook in Dashboard, choose a header name and value, and Paxos sends that header on every delivery. Your endpoint authenticates the request by comparing it with the value you stored.

Events are **thin**: `{id, type, source, time, object, is_test}`. Fetch the full object with the Get Event API using the event `id`.

## When to Use This Skill

* How do I receive Paxos webhooks?
* Does Paxos sign webhook payloads? (No: use the API Key header you configure)
* How do I authenticate requests from a Paxos webhook consumer?
* How do I handle `identity.approved`, `transfer.crypto_deposit.completed` or `orchestration.completed` events?
* Why does my Paxos webhook endpoint return 401?

## Verification (core)

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

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyPaxosRequest(headers, secret = process.env.PAXOS_WEBHOOK_SECRET,
                            headerName = process.env.PAXOS_WEBHOOK_HEADER || 'Authorization') {
  if (!secret) return false;                      // never accept when unconfigured
  const received = headers[headerName.toLowerCase()]; // Node lowercases header names
  return typeof received === 'string' && received !== '' && safeEqual(received, secret);
}

```

* The header name is whatever you typed as **API Key Header** in Dashboard; the examples read it from `PAXOS_WEBHOOK_HEADER` (default `Authorization`) and look it up case-insensitively.
* `PAXOS_WEBHOOK_SECRET` is the **exact, whole header value**, including any `Bearer ` prefix you typed. Compare the whole value; don't strip or parse a prefix.
* In Python use `hmac.compare_digest(a.encode(), b.encode())` (never pass `str`, which raises on non-ASCII).
* Return `401` for a missing or mismatched header, `400` for a body that isn't a JSON event, `200` once handled.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.
* **Security note:** a static shared credential authenticates the *sender*, not the *body*, and gives **no replay protection** (no signature, no timestamp). Use HTTPS only and rotate the credential if it leaks. Dedupe on the event `id`.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/paxos-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/paxos-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/paxos-webhooks/examples/fastapi/).

## Event Types

Webhook `type` values come from the Paxos Webhooks API Reference. Common ones:

| Event | Sent When |
| --- | --- |
| `identity.approved` | An identity verification is approved |
| `identity.denied` | An identity verification is denied |
| `identity.documents_required` | Additional documents are required |
| `identity.documents_accepted` / `identity.documents_denied` | An Enhanced Due Diligence review is accepted / denied |
| `transfer.crypto_deposit.completed` | A crypto deposit completes |
| `transfer.wire_withdrawal.failed` | A wire withdrawal fails |
| `orchestration.completed` | A stablecoin orchestration completes |

Transfers follow `transfer.<method>_<deposit|withdrawal>.<pending|completed|failed>` (methods include `act`, `wire`, `rtp`, `crypto`, `cubix`, `bny_intrabank`, `scb_intrabank`, `banking_circle_intrabank`). Orchestrations are `orchestration.<processing|pending|rejected|completed|failed>` plus `orchestration.status_details.*`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/paxos-webhooks/references/overview.md).

Events sent from the Dashboard **Test** feature have `is_test: true`; ignore them.

## Environment Variables

```bash
PAXOS_WEBHOOK_HEADER=X-Paxos-Webhook-Secret   # API Key Header name set in Dashboard (default: Authorization)
PAXOS_WEBHOOK_SECRET=your-api-key-value       # exact API Key value, including any "Bearer " prefix

```

## Local Development

Paxos can't reach `localhost` (and requires an HTTPS URL). Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 paxos --path /webhooks/paxos

```

Use the printed URL as the webhook URL in Dashboard.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/paxos-webhooks/references/overview.md) - What Paxos webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/paxos-webhooks/references/setup.md) - Create the webhook in Dashboard with API Key auth
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/paxos-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/paxos-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests