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

# Set up webhooks for real-time payment notifications

> Register a CoinVoyage webhook endpoint, verify HMAC-SHA256 delivery signatures, and handle order lifecycle events in your server.

Webhooks let you receive real-time notifications whenever an order changes status. Rather than polling the CoinVoyage API for updates, you register an HTTPS endpoint and CoinVoyage delivers a `POST` request with the event details each time something happens: payment created, awaiting payment, confirming, executing, completed, refunded, expired, failed, or partially paid.

## Set up a webhook

Register and manage webhook endpoints in the CoinVoyage Dashboard under the **Developers** section.

<Steps>
  <Step title="Navigate to Developers -> Webhooks">
    Open the [CoinVoyage Dashboard](https://dashboard.coinvoyage.io/developers), select **Developers** in the sidebar, then click **Webhooks**.
  </Step>

  <Step title="Click Add Webhook">
    Click the **Add Webhook** button to open the registration form.
  </Step>

  <Step title="Enter your endpoint URL">
    Provide the URL of your publicly accessible webhook handler.

    <Warning>
      Your endpoint must use HTTPS in production. HTTP endpoints are not accepted.
    </Warning>
  </Step>

  <Step title="Select the events to subscribe to">
    Choose which order lifecycle events should trigger delivery to your endpoint. Subscription event identifiers use uppercase `ORDER_*` format, for example `ORDER_COMPLETED`.
  </Step>

  <Step title="Save and store your Webhook Secret">
    Save the webhook. CoinVoyage generates a **Webhook Secret**. Store it securely, for example as `COIN_VOYAGE_WEBHOOK_SECRET`. You need this secret to verify the signature on every incoming request.

    <Warning>
      Store the Webhook Secret in an environment variable, never in source code or version control.
    </Warning>
  </Step>
</Steps>

<Info>
  Your endpoint must be publicly accessible and respond with a `2xx` status code within 30 seconds. Responses outside this window are treated as delivery failures.
</Info>

## Delivery payload

CoinVoyage v3 delivers the same event envelope to registered webhooks and `/v3/ws` subscribers:

```json theme={null}
{
  "event": "ORDER_COMPLETED",
  "delivered_at": "2026-06-23T12:34:56Z",
  "order": {
    "id": "cabc1234567890abcdef12",
    "mode": "SALE",
    "status": "COMPLETED",
    "metadata": {
      "order_id": "order_123"
    }
  }
}
```

Use `event` to dispatch business logic and `order.id` as the CoinVoyage payment lifecycle ID. Use `order.metadata` to reconcile back to your internal order, invoice, account, or customer.

## Verify webhook signatures

Every webhook request includes a `CoinVoyage-Webhook-Signature` header containing an HMAC-SHA256 signature of the raw request body, encoded in Base64. Always verify this signature before parsing or acting on the payload.

The example below shows a complete Next.js Route Handler that verifies the signature and dispatches on the event identifier:

```typescript app/api/webhook/route.ts theme={null}
import { Buffer } from "buffer";
import { createHmac, timingSafeEqual } from "crypto";

const webhookSecret = process.env.COIN_VOYAGE_WEBHOOK_SECRET!;

type CoinVoyageWebhookEvent = {
  event: string;
  delivered_at: string;
  order: {
    id: string;
    status: string;
    metadata?: Record<string, unknown>;
  };
};

export const POST = async (req: Request) => {
  const rawBody = await req.text();
  const signature = req.headers.get("CoinVoyage-Webhook-Signature");

  const expected = createHmac("sha256", webhookSecret)
    .update(rawBody)
    .digest("base64");

  const signatureBytes = Buffer.from(signature ?? "");
  const expectedBytes = Buffer.from(expected);

  if (
    signatureBytes.length !== expectedBytes.length ||
    !timingSafeEqual(signatureBytes, expectedBytes)
  ) {
    return new Response("Unauthorized", { status: 401 });
  }

  const event = JSON.parse(rawBody) as CoinVoyageWebhookEvent;

  switch (event.event) {
    case "ORDER_COMPLETED":
      console.log("Order completed", event.order.id);
      break;
    case "ORDER_REFUNDED":
      console.log("Order refunded", event.order.id);
      break;
    case "ORDER_EXPIRED":
      console.log("Order expired", event.order.id);
      break;
    default:
      console.log("Unhandled webhook", event.event, event.order.id);
  }

  return new Response("Webhook received", { status: 200 });
};
```

<Note>
  Read the raw request body with `req.text()` before calling `JSON.parse`. Web Fetch API request bodies can only be read once, so read once as text, verify the signature, then parse.
</Note>

## Security best practices

* Verify the signature before parsing or acting on the payload.
* Use a constant-time comparison such as `timingSafeEqual` for signature checks.
* Use HTTPS in production.
* Store your Webhook Secret securely in a server-side secret store.
* Return a `2xx` response quickly and move heavy work to a queue or background job.
* Make event handling idempotent by `event`, `delivered_at`, and `order.id`, plus your own internal ID from `order.metadata`.
* Return `200` for event types you intentionally ignore so delivery does not retry forever.
