Skip to main content
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.
1

Navigate to Developers -> Webhooks

Open the CoinVoyage Dashboard, select Developers in the sidebar, then click Webhooks.
2

Click Add Webhook

Click the Add Webhook button to open the registration form.
3

Enter your endpoint URL

Provide the URL of your publicly accessible webhook handler.
Your endpoint must use HTTPS and be reachable from the public internet. HTTP URLs, localhost, and private or internal addresses are rejected. To test against a machine on your own network, expose it through a public HTTPS tunnel.
4

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

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.
Store the Webhook Secret in an environment variable, never in source code or version control.
Your endpoint must be publicly accessible and respond with a 2xx status code within 30 seconds. A slower response, any other status code, or a network error counts as a failed delivery and is retried.

Delivery payload

CoinVoyage v3 delivers the same event envelope to registered webhooks and /v3/ws subscribers:
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.

Delivery headers

Every webhook request, including each retry, carries these headers:

Verify webhook signatures

Always verify the signature before parsing or acting on the payload:
  1. Read the raw request body exactly as it was received. Do not parse and re-serialize it first.
  2. Compute base64(HMAC-SHA256(secret, "<timestamp>.<raw body>")), using the value of the CoinVoyage-Webhook-Timestamp header as the timestamp.
  3. Compare the result with the CoinVoyage-Webhook-Signature-V2 header using a constant-time comparison.
  4. Reject the request if the timestamp is more than 5 minutes away from your server’s current time.
Because the timestamp is part of the signed content, a captured request cannot be sent again later with a fresh timestamp.

With the SDK

verifyWebhookSignature, exported from @coin-voyage/paykit/server, performs all four steps. It takes the raw body, the request headers, and your Webhook Secret. The example below shows a complete Next.js Route Handler that verifies the signature and dispatches on the event identifier:
app/api/webhook/route.ts
The helper never throws. It returns an object, so check result.valid rather than the result itself. On failure, result.error.code is one of missing_secret, invalid_body, invalid_options, missing_headers, invalid_timestamp, invalid_signature or timestamp_out_of_tolerance. The helper accepts an options object as its last argument:
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.

Without the SDK

The same check needs only Node’s built-in crypto module:
webhook-server.mjs
Node lowercases incoming header names, which is why the example reads coinvoyage-webhook-timestamp. In frameworks that parse JSON for you, such as Express, configure the route to give you the raw body. A re-serialized body does not match the signature.

Legacy signature

CoinVoyage-Webhook-Signature is still sent on every delivery so existing endpoints keep working. It covers only the body and has no timestamp, so it cannot tell a fresh delivery from an old one that is sent again. Verify CoinVoyage-Webhook-Signature-V2 in new integrations and move existing ones to it.

Retries

A delivery fails when your endpoint returns anything other than a 2xx status code, does not respond within 30 seconds, or cannot be reached. CoinVoyage retries a failed delivery after 1 minute, 5 minutes, 15 minutes, 1 hour, 2 hours, and 4 hours, then every 8 hours. Retries stop at the first 2xx response. A delivery that has not succeeded 72 hours after the event was created is abandoned and is not attempted again. Each attempt is a fresh POST with a new CoinVoyage-Webhook-Timestamp and a new CoinVoyage-Webhook-Signature-V2, so a retry that arrives hours after the event still passes the 5-minute check. X-Event-ID does not change between attempts. Check freshness against the timestamp header, not against delivered_at in the body, which is the time the event was published.
Deliveries are at-least-once. The same event can arrive more than once, for example when your endpoint processed a request but responded too late. Record each X-Event-ID you have processed and acknowledge repeats with a 2xx without processing them again.

Security best practices

  • Verify the signature before parsing or acting on the payload.
  • Verify CoinVoyage-Webhook-Signature-V2 and reject timestamps more than 5 minutes from the current time.
  • Use a constant-time comparison such as timingSafeEqual for signature checks.
  • Keep your server clock synchronized so the timestamp check does not reject valid deliveries.
  • 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 de-duplicating on the X-Event-ID header, and use your own internal ID from order.metadata to reconcile.
  • Return 200 for event types you intentionally ignore so they are not retried for 72 hours.