Webhooks & Redirects
CryptumPay notifies your backend about payment events in two ways:
- Webhooks — HTTP
POSTrequests sent to your configured endpoint when an order's state changes - Redirects — The payment gate redirects the customer's browser to your
successPathorfailPathafter payment completion
Webhooks
Setup
Configure your webhook URL in CryptumPay Console on the project page. The URL must be reachable from the internet and must use HTTPS with a valid certificate.
HTTPS Only
The webhook delivery service validates the SSL certificate of your endpoint. HTTP or self-signed certificates will cause delivery failure.
Incoming Request Format
Webhooks are delivered as POST requests with the following headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Signature | HMAC signature for verifying the payload (see Validating Webhooks) |
X-IDMP-Key | UUID — idempotency key; the same event may be retried with the same key |
User-Agent | CryptumPayNotifier |
Your endpoint must respond with a 2xx status code to acknowledge receipt. Any other response (including redirects) is treated as a failure and triggers a retry.
Delivery Retries
Failed deliveries are retried with exponential backoff up to 12 attempts over approximately 1 hour. After 12 failed attempts the webhook is discarded.
Use X-IDMP-Key to deduplicate events in case of retries.
Webhook Types
merchantOrder
Sent when a merchant order is created.
{
"type": "merchantOrder",
"id": "1c885afe-369f-4b38-92fa-983660c2452d",
"title": "Donation",
"description": "Support our project",
"url": null,
"fiatCurrency": "usd",
"fiatAmount": "0.8",
"projectDataUserId": null,
"projectDataUserEmail": null,
"projectDataMeta": null,
"projectDataOrderId": null,
"createdAt": "2026-04-18T01:50:40.235Z",
"updatedAt": "2026-04-18T01:50:40.346Z",
"expiresAt": "2026-04-25T01:50:40.234Z"
}customerOrder
Sent every time the customer order changes status. Expect multiple webhooks for the same id as the order progresses through the payment lifecycle.
Status Lifecycle
created → pending → crediting → finished| Status | Meaning |
|---|---|
created | Customer has selected a cryptocurrency and the deposit address has been assigned. Awaiting payment. |
pending | An incoming transaction has been detected on-chain. Waiting for block confirmations. |
crediting | Sufficient confirmations received. The payment is being credited to the merchant balance. |
finished | Payment fully completed. The income field contains the fiat amount credited after all fees. |
Example: created
{
"type": "customerOrder",
"id": "6fcfa357-7bd5-4f3e-9abc-5a85ab7c1221",
"merchantOrderId": "1c885afe-369f-4b38-92fa-983660c2452d",
"cryptoCurrency": "bnb",
"cryptoAmount": "0.001355",
"status": "created",
"createdAt": "2026-04-18T01:50:40.341Z",
"updatedAt": "2026-04-18T01:50:40.341Z",
"expiresAt": "2026-04-18T02:20:40.340Z"
}Example: finished
{
"type": "customerOrder",
"id": "6fcfa357-7bd5-4f3e-9abc-5a85ab7c1221",
"merchantOrderId": "1c885afe-369f-4b38-92fa-983660c2452d",
"cryptoCurrency": "bnb",
"cryptoAmount": "0.001355",
"status": "finished",
"income": "0.799169",
"createdAt": "2026-04-18T01:50:40.341Z",
"updatedAt": "2026-04-18T01:52:19.207Z",
"expiresAt": "2026-04-18T02:20:40.340Z"
}TIP
The income field (fiat amount credited after fees) is present only in the finished status webhook.
Validating Webhooks
Always validate the X-Signature header before processing a webhook. This ensures the payload was genuinely sent by CryptumPay and has not been tampered with.
Why It Matters
Without validation, anyone who knows your webhook URL can send fake payment notifications, potentially triggering order fulfillment for payments that never happened.
Algorithm (language-independent)
Get the raw request body — before any JSON parsing. The body must be the exact bytes as received.
Hash the body
bodyHash = hex(SHA-256(rawBody))Build the prehash string
prehash = bodyHash + "|" + apiKeyCompute the expected signature
expectedSignature = hex(HMAC-SHA256(prehash, secret))Compare signatures
CompareexpectedSignaturewith the value ofX-Signature.
Use a constant-time comparison to prevent timing attacks.
Example: Node.js SDK
import { CryptumPaySigner } from '@cryptumpay/node-sdk';
const signer = new CryptumPaySigner(
process.env.CRYPTUMPAY_API_KEY,
process.env.CRYPTUMPAY_API_SECRET
);
// In your webhook handler (Express example):
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'] as string;
// Pass the raw Buffer or string — do NOT pass req.body after JSON.parse
const isValid = signer.verifyCallback(signature, req.body.toString());
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
// Process the event
console.log('Received event:', event.type, event.id);
res.status(200).send('OK');
});Read raw body
Use express.raw() (or equivalent) instead of express.json() for the webhook route. JSON.stringify(JSON.parse(body)) may produce a different string than the original, breaking signature verification.
Example: Manual validation (Node.js)
import crypto from 'crypto';
function verifyWebhook(
rawBody: string,
signature: string,
apiKey: string,
secret: string
): boolean {
const bodyHash = crypto.createHash('sha256').update(rawBody).digest('hex');
const prehash = bodyHash + '|' + apiKey;
const expected = crypto.createHmac('sha256', secret).update(prehash).digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}Redirects
In addition to webhooks, CryptumPay redirects the customer's browser to your site after payment completion. These redirects are configured per domain in the console.
Setup
In the CryptumPay Console, navigate to your project → Domain. For each domain you can configure:
successPath— path to redirect to after a successful paymentfailPath— path to redirect to when a payment expires or is cancelled
Paths must start with /, for example /payment/success or /checkout/cancelled.
Redirect URL Format
The customer is redirected to:
https://{host}{successPath}?cpayorder={orderId}&cpayproject={projectId}or on failure/cancellation:
https://{host}{failPath}?cpayorder={orderId}&cpayproject={projectId}Query Parameters
| Parameter | Description |
|---|---|
cpayorder | The merchant order ID |
cpayproject | Your project ID |
Use these to look up the order in your system and display the appropriate confirmation page.
Backend Verification
Never rely on the redirect alone to confirm a payment. Always verify the final order status server-side — via webhooks or by calling GET /v1/orders/:orderId.
Example: Handling a Success Redirect
// Express handler for /payment/success
app.get('/payment/success', async (req, res) => {
const orderId = req.query.cpayorder as string;
// Verify server-side
const response = await client.getOrder(orderId);
if (response.errorObject) {
return res.redirect('/payment/failed');
}
const status = response.data.financeSummary.status;
// status 3 = Paid, status 4 = Crediting — see financeSummary Statuses in API Endpoints
const isPaid = status === 3 || status === 4;
if (!isPaid) {
return res.redirect('/payment/failed');
}
// Mark order as paid in your database
await db.orders.markPaid(orderId, response.data.financeSummary.income);
res.render('payment-success', { orderId });
});Confirming a successful payment
To confirm that funds were received, check that financeSummary.status is 3 or 4:
- Status 3 (Paid) — funds have already been credited to your project balance.
- Status 4 (Crediting) — crediting is still in progress, but the system has definitely received the payment and will complete the transfer shortly.
See financeSummary Statuses for the full list of possible values.
See Also
- Authentication — API keys and request signing
- API Endpoints — Full endpoint reference