Webhooks
Cryptonly signs outbound webhooks sent to your configured webhook URLs. This helps your server verify that webhook payloads were sent by Cryptonly and were not modified in transit.
Webhooks are sent for events such as invoice, deposit, and withdrawal status updates.
Invoice, deposit, and withdrawal webhooks are sent on every status change, not only when the resource reaches a final state.
How webhook signing works
Each webhook request includes a signature header:
x-webhook-signature: <signature>The signature is generated using HMAC-SHA256 over the exact raw JSON request body.
Your server should calculate the same signature using your Cryptonly webhook signing key. If the calculated signature matches the received signature, the webhook can be trusted.
Webhook HTTP contract
Method
POST
Content type
application/json
Signature header
x-webhook-signature
Signature format
Lowercase hex string
Encoding
UTF-8 JSON body
Example webhook body:
{
"event": "invoice.statusChanged",
"data": {},
"timestamp": "2026-01-01T12:00:00.000Z"
}The timestamp field is included in every webhook body and can be used for replay protection.
Webhook payload shape
The data object contains a snapshot of the resource at the time of the event. All event types may include an optional previousStatus field — the status immediately before this transition. On invoice create, previousStatus is omitted.
Event
data shape
invoice.statusChanged
Invoice object (same fields as merchant GET invoice) + optional previousStatus
deposit.statusChanged
Deposit object + optional previousStatus and deposit-specific fields below
withdrawal.statusChanged
Withdrawal object (same fields as merchant GET withdrawal, with tenantAccountId sent as accountId) + optional previousStatus
Deposit-specific optional fields in data:
addressProvisionId— present when the deposit is a payment under an address provision sessiondepositTransactionHashactuallyReceivedAmountactuallyReceivedAmountUsd
For full field lists and status values, see:
Supported webhook events
Cryptonly currently sends signed webhooks for status changes such as:
invoice.statusChanged
Sent when an invoice status changes
deposit.statusChanged
Sent when a standalone deposit or address-provision payment status changes
withdrawal.statusChanged
Sent when a withdrawal status changes
Webhook events are emitted whenever the resource status changes.
For example, an invoice may trigger multiple webhook events during its lifecycle.
When events fire
invoice.statusChanged
On invoice create (initial created) and on every subsequent status transition
deposit.statusChanged
On each deposit status transition only (not on initial create)
withdrawal.statusChanged
On each post-commit status transition; the first webhook typically arrives when the withdrawal moves to processing (see Withdrawal statuses)
Deposit webhooks are not sent when a deposit is linked to an invoice (invoiceId is set). For invoice checkout payments, listen for invoice.statusChanged instead.
No webhook is sent if no webhook URL resolves for the resource (see Default webhook URLs below).
Default webhook URLs
You can configure default webhook URLs in the Cryptonly merchant dashboard.
Go to:
You can configure default HTTPS webhook endpoints for invoices, deposits, and withdrawals.
When creating an invoice, deposit, address provision, or committing a withdrawal:
if the request includes a
webhookUrl, that URL is used;if the request does not include a
webhookUrl, Cryptonly uses the tenant default for that resource type;an explicit
webhookUrlin the request always takes priority.
Per-resource URL resolution:
Invoice
Request webhookUrl → defaultInvoiceWebhookUrl
Withdrawal
Request webhookUrl → defaultWithdrawalWebhookUrl
Deposit
Request webhookUrl → address provision webhookUrl (if applicable) → defaultDepositWebhookUrl
If no URL resolves after the cascade, Cryptonly does not send a webhook for that event.
Webhook URLs must use HTTPS.
Getting your webhook signing key
The webhook signing key is available in the Cryptonly admin dashboard.
Go to:
The Webhook signing key section is shown under the security settings.
Only organization admins can rotate and access the full signing key.
The full webhook signing key is shown only once after rotation. Copy it immediately and store it securely.
How to obtain the key
Open the Cryptonly merchant/admin dashboard.
Go to Settings.
Open the Security tab.
Find the Webhook signing key section.
Click Rotate key.
Confirm the rotation.
Copy the new signing key immediately.
Store it in your server environment variables.
Example:
Important key rotation behavior
When you rotate the webhook signing key:
new webhook deliveries are signed with the new key;
your server must be updated to use the new key;
verification with the old key will fail for new webhooks.
If your integration has never copied a signing key before, rotate the key once and use the newly generated value for your webhook verifier.
Signature format
Cryptonly uses:
Where:
rawBody
Exact raw UTF-8 bytes of the JSON body sent by Cryptonly
webhookSigningKey
Your tenant webhook signing key
Output
Lowercase hex string
Do not parse the JSON body and then stringify it again before verification. The signature must be calculated using the raw request body exactly as received.
Replay protection
The webhook signature proves that the request body was signed with your Cryptonly signing key.
However, the signature alone does not prevent replay attacks. If an attacker captures a valid webhook request, they could try to send the same request again.
To reduce replay risk, your server should do at least one of the following:
reject webhooks with old
timestampvalues;deduplicate events using stable identifiers from
data;store processed invoice, deposit, or withdrawal status transitions.
Example replay protection strategy:
You can also deduplicate by values such as:
invoice
id;invoice
orderId;deposit
id;withdrawal
id;event type;
current status;
previous status.
Recommended verification flow
Your webhook endpoint should follow this order:
Receive the webhook request.
Read the raw request body.
Read the
x-webhook-signatureheader.Calculate the expected signature using your webhook signing key.
Compare signatures using a timing-safe comparison.
Reject invalid signatures with
401 Unauthorized.Parse the JSON body only after signature verification succeeds.
Optionally validate timestamp and deduplicate the event.
Process the webhook.
Return a
2xxresponse.
Return a successful 2xx response only after the webhook has been accepted by your server.
Verifying webhooks in Express
Use express.raw() for the webhook route so you can verify the exact raw request body.
Verifying with @cryptonly/sdk
If you use the Cryptonly SDK, you can verify invoice, deposit, and withdrawal webhooks with helper functions.
The route must still use a raw body parser. SDK verification also depends on the exact raw request body.
Framework notes
Some frameworks parse JSON automatically. For Cryptonly webhook verification, configure raw body handling for the webhook route.
Examples:
Express
Use express.raw({ type: 'application/json' }) on the webhook route
Next.js
Disable body parser for the API route and read the raw request stream
Fastify
Configure a raw body parser for application/json on the webhook route
Do not use the parsed JSON object for signature verification.
Testing webhook verification
You can send a test webhook from the Cryptonly dashboard.
Go to:
Three test kinds are available:
deposit
Sample invoice.statusChanged webhook
merchant_deposit
deposit.statusChanged
withdrawal
withdrawal.statusChanged
Use this to verify that:
your webhook URL is reachable;
your server receives the request;
your server verifies the signature successfully;
your handler returns a
2xxresponse;your event processing logic works as expected.
To verify webhooks fired from a real (or simulated) status change instead of a synthetic one, see Testing your integration.
Delivery behavior and retries
Cryptonly expects your server to respond with a successful 2xx status code.
Success
Any 2xx response is treated as successful
Failure
Non-2xx responses are treated as failed
Timeout
Webhook delivery has a 10-second timeout
Invalid signature
Your server should return 401 Unauthorized
Retry policy
Cryptonly makes up to 7 delivery attempts in total
Retry delay
Retries use exponential backoff starting from 30 seconds
If your endpoint is temporarily unavailable, Cryptonly will retry delivery automatically.
The retry delay grows after each failed attempt. In practice, this means retries are spaced out approximately like this:
1st retry
30 seconds
2nd retry
1 minute
3rd retry
2 minutes
4th retry
4 minutes
5th retry
8 minutes
6th retry
16 minutes
With 7 total attempts, this usually means:
Retry timing may vary slightly depending on queue load and infrastructure conditions.
Your handler should avoid long-running processing before responding. If needed, store the event and process it asynchronously.
Handling duplicate events
Your webhook handler should be idempotent.
This means receiving the same event more than once should not cause duplicated business actions.
Recommended deduplication key:
Examples:
If your system already processed the same status transition, return 200 OK and do not repeat the action.
Security checklist
Before going live, make sure that:
your webhook URL uses HTTPS;
your server verifies
x-webhook-signature;invalid signatures are rejected with
401 Unauthorized;verification uses the raw request body;
signature comparison is timing-safe;
the webhook signing key is stored securely;
replay protection or event deduplication is implemented;
webhook processing is idempotent;
test webhooks pass successfully.
Common mistakes
Parsing JSON before verification
Do not verify using a re-stringified JSON object.
Use the raw request body.
Using Base64
Cryptonly webhook signatures are lowercase hex strings, not Base64.
Decoding the signing key as hex bytes
Use the signing key string as provided.
Ignoring replay protection
A valid signature does not automatically prevent a captured request from being replayed.
Use timestamp checks and/or deduplication.
Returning success before validation
Verify the signature before doing meaningful work or returning success.
Summary
Cryptonly webhook signing allows your server to verify that webhook requests were sent by Cryptonly and that the payload was not modified.
To verify webhooks correctly:
Get your webhook signing key from Settings → Security.
Configure your webhook URL in Settings → Integration.
Use a raw body parser for your webhook route.
Verify
x-webhook-signaturewith HMAC-SHA256.Reject invalid signatures.
Add timestamp checks or deduplication.
Process the webhook idempotently.
Last updated