Skip to content

Webhooks & Redirects

CryptumPay notifies your backend about payment events in two ways:

  • Webhooks — HTTP POST requests sent to your configured endpoint when an order's state changes
  • Redirects — The payment gate redirects the customer's browser to your successPath or failPath after 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:

HeaderDescription
Content-Typeapplication/json
X-SignatureHMAC signature for verifying the payload (see Validating Webhooks)
X-IDMP-KeyUUID — idempotency key; the same event may be retried with the same key
User-AgentCryptumPayNotifier

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.

json
{
  "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
StatusMeaning
createdCustomer has selected a cryptocurrency and the deposit address has been assigned. Awaiting payment.
pendingAn incoming transaction has been detected on-chain. Waiting for block confirmations.
creditingSufficient confirmations received. The payment is being credited to the merchant balance.
finishedPayment fully completed. The income field contains the fiat amount credited after all fees.

Example: created

json
{
  "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

json
{
  "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)

  1. Get the raw request body — before any JSON parsing. The body must be the exact bytes as received.

  2. Hash the body
    bodyHash = hex(SHA-256(rawBody))

  3. Build the prehash string
    prehash = bodyHash + "|" + apiKey

  4. Compute the expected signature
    expectedSignature = hex(HMAC-SHA256(prehash, secret))

  5. Compare signatures
    Compare expectedSignature with the value of X-Signature.
    Use a constant-time comparison to prevent timing attacks.

Example: Node.js SDK

typescript
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)

typescript
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 payment
  • failPath — 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

ParameterDescription
cpayorderThe merchant order ID
cpayprojectYour 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

typescript
// 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

Released under the MIT License.