For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

Item
Value

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 session

  • depositTransactionHash

  • actuallyReceivedAmount

  • actuallyReceivedAmountUsd

For full field lists and status values, see:


Supported webhook events

Cryptonly currently sends signed webhooks for status changes such as:

Event
Description

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

Event
When Cryptonly sends it

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 webhookUrl in the request always takes priority.

Per-resource URL resolution:

Resource
Resolution order

Invoice

Request webhookUrldefaultInvoiceWebhookUrl

Withdrawal

Request webhookUrldefaultWithdrawalWebhookUrl

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.


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.

How to obtain the key

  1. Open the Cryptonly merchant/admin dashboard.

  2. Go to Settings.

  3. Open the Security tab.

  4. Find the Webhook signing key section.

  5. Click Rotate key.

  6. Confirm the rotation.

  7. Copy the new signing key immediately.

  8. 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:

Part
Description

rawBody

Exact raw UTF-8 bytes of the JSON body sent by Cryptonly

webhookSigningKey

Your tenant webhook signing key

Output

Lowercase hex string


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 timestamp values;

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


Your webhook endpoint should follow this order:

  1. Receive the webhook request.

  2. Read the raw request body.

  3. Read the x-webhook-signature header.

  4. Calculate the expected signature using your webhook signing key.

  5. Compare signatures using a timing-safe comparison.

  6. Reject invalid signatures with 401 Unauthorized.

  7. Parse the JSON body only after signature verification succeeds.

  8. Optionally validate timestamp and deduplicate the event.

  9. Process the webhook.

  10. Return a 2xx response.

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.


Framework notes

Some frameworks parse JSON automatically. For Cryptonly webhook verification, configure raw body handling for the webhook route.

Examples:

Framework
Recommendation

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:

Kind
Event sent

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 2xx response;

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

Behavior
Description

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:

Retry
Approximate delay after previous failed attempt

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:

  1. Get your webhook signing key from Settings → Security.

  2. Configure your webhook URL in Settings → Integration.

  3. Use a raw body parser for your webhook route.

  4. Verify x-webhook-signature with HMAC-SHA256.

  5. Reject invalid signatures.

  6. Add timestamp checks or deduplication.

  7. Process the webhook idempotently.

Last updated