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

# Webhooks

> Never miss a payment — instant event delivery so your system reacts the moment funds arrive.

Webhooks deliver real-time notifications about payment events. Prism POSTs a signed JSON payload to your configured endpoint whenever a payment is completed, fails, or settles. Use webhooks to update order status, trigger fulfillment, log transactions, or sync with your backend.

## Setup

<Steps>
  <Step title="Create an endpoint">
    Add an HTTP POST endpoint to your application that accepts JSON payloads.
  </Step>

  <Step title="Register in the Console">
    Go to [Prism Console](https://apps.fd.xyz/prism/) → **Settings → Webhooks**
    → **Add Endpoint**. Enter your URL and select the events you want to
    receive.
  </Step>

  <Step title="Copy the signing secret">
    The Console generates a signing secret for your endpoint. Copy it — you'll
    need it to verify webhook signatures.
  </Step>

  <Step title="Verify signatures">
    Always verify the `X-Prism-Signature` header before processing events. See
    [Signature Verification](#signature-verification) below.
  </Step>
</Steps>

### Example Endpoint

```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.raw({ type: "application/json" }));

const WEBHOOK_SECRET = process.env.PRISM_WEBHOOK_SECRET;

app.post("/webhooks/prism", (req, res) => {
  const signature = req.headers["x-prism-signature"] as string;
  const payload = req.body.toString();

  // Verify signature
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(payload)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(payload);

  switch (event.type) {
    case "payment.completed":
      // Payment verified and settled on-chain
      handlePaymentCompleted(event.data);
      break;
    case "payment.failed":
      // Payment verification failed
      handlePaymentFailed(event.data);
      break;
    case "settlement.completed":
      // Funds settled to merchant wallet
      handleSettlement(event.data);
      break;
  }

  // Respond 200 quickly — process asynchronously if needed
  res.status(200).send("OK");
});
```

## Event Types

| Event                  | Description                              | Triggered When                                |
| ---------------------- | ---------------------------------------- | --------------------------------------------- |
| `payment.pending`      | Payment submitted, awaiting confirmation | After agent submits payment on-chain          |
| `payment.completed`    | Payment verified and settled             | After on-chain confirmation by Spectrum       |
| `payment.failed`       | Payment verification failed              | Invalid transaction, wrong amount, or expired |
| `settlement.completed` | Funds settled to merchant wallet         | After Spectrum settlement completes           |

## Payload Format

All webhook payloads follow the same structure:

```json theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "id": "evt_abc123def456",
  "type": "payment.completed",
  "created": "2025-01-15T10:30:00Z",
  "data": {
    "payment_id": "pay_xyz789ghi012",
    "amount": "10000",
    "token": "USDC",
    "chain": "base",
    "from": "0xAgentWallet1234567890abcdef1234567890abcdef",
    "to": "0xMerchantWallet1234567890abcdef1234567890ab",
    "tx_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "endpoint": "/api/premium/data",
    "status": "completed"
  }
}
```

| Field             | Description                                     |
| ----------------- | ----------------------------------------------- |
| `id`              | Unique event identifier                         |
| `type`            | Event type (see table above)                    |
| `created`         | ISO 8601 timestamp                              |
| `data.payment_id` | Prism payment/charge identifier                 |
| `data.amount`     | Amount in token base units                      |
| `data.token`      | Token symbol (FDUSD, USDC)                      |
| `data.chain`      | Chain where settlement occurred                 |
| `data.from`       | Agent wallet address                            |
| `data.to`         | Merchant wallet address                         |
| `data.tx_hash`    | On-chain transaction hash                       |
| `data.endpoint`   | The merchant endpoint that triggered the charge |
| `data.status`     | Payment status                                  |

## Signature Verification

All webhooks include an `X-Prism-Signature` header containing an HMAC-SHA256 signature of the raw request body, using your webhook signing secret as the key.

**Always verify signatures before processing events.** This prevents spoofed requests from triggering actions in your application.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
    import crypto from "crypto";

    function verifyWebhookSignature(
      payload: string,
      signature: string,
      secret: string
    ): boolean {
      const expected = crypto
        .createHmac("sha256", secret)
        .update(payload)
        .digest("hex");
      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expected)
      );
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
    import hmac
    import hashlib

    def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
        expected = hmac.new(
            secret.encode(),
            payload,
            hashlib.sha256
        ).hexdigest()
        return hmac.compare_digest(signature, expected)
    ```
  </Tab>
</Tabs>

<Warning>
  Use constant-time comparison (`timingSafeEqual` / `hmac.compare_digest`) to
  prevent timing attacks. Do not use `===` or `==` for signature comparison.
</Warning>

## Retry Policy

If your endpoint returns a non-2xx response, times out, or is unreachable, Prism retries delivery with exponential backoff:

| Attempt | Delay After Failure |
| ------- | ------------------- |
| 1       | Immediate           |
| 2       | 5 minutes           |
| 3       | 30 minutes          |
| 4       | 2 hours             |
| 5       | 24 hours            |

After 5 failed attempts, the event is marked as failed in the Console. You can view delivery logs and manually retry from **Settings → Webhooks → Delivery Log**.

**Best practices:**

* Respond with 200 as quickly as possible — do heavy processing asynchronously
* Use a queue (SQS, Redis, etc.) for webhook processing in high-volume scenarios
* Implement idempotency using the event `id` to handle duplicate deliveries

## Testing Webhooks

* **Console test button** — Send a test event from the Prism Console to verify your endpoint is reachable and responding correctly
* **Local development** — Use a tunneling tool like [ngrok](https://ngrok.com) to expose your local endpoint for testing
* **Small amount testing** — Use production with small amounts to verify your full webhook pipeline end-to-end
