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.
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.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:
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:- Read the raw request body exactly as it was received. Do not parse and re-serialize it first.
- Compute
base64(HMAC-SHA256(secret, "<timestamp>.<raw body>")), using the value of theCoinVoyage-Webhook-Timestampheader as the timestamp. - Compare the result with the
CoinVoyage-Webhook-Signature-V2header using a constant-time comparison. - Reject the request if the timestamp is more than 5 minutes away from your server’s current time.
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
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-incrypto module:
webhook-server.mjs
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 a2xx 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.
Security best practices
- Verify the signature before parsing or acting on the payload.
- Verify
CoinVoyage-Webhook-Signature-V2and reject timestamps more than 5 minutes from the current time. - Use a constant-time comparison such as
timingSafeEqualfor 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
2xxresponse quickly and move heavy work to a queue or background job. - Make event handling idempotent by de-duplicating on the
X-Event-IDheader, and use your own internal ID fromorder.metadatato reconcile. - Return
200for event types you intentionally ignore so they are not retried for 72 hours.