Gareth Wilson Gareth Wilson

Guide to CryptoProcessing Webhooks: Features and Best Practices

Published


CryptoProcessing, from CoinsPaid, lets online businesses accept and pay out cryptocurrency: deposit addresses for customers, withdrawals to external wallets, conversions between currencies and one-off payment requests. Blockchain transactions take time to confirm, and some never do, so each transaction passes through several states after your API call returns. CryptoProcessing's callbacks report each of those state changes, and they are how a casino, exchange or merchant knows when to credit a balance or mark a payout as sent.

This guide covers the callback kinds and their statuses, how to verify the HMAC-SHA512 signature with the right key, and how CryptoProcessing's retry schedule and its 200 versus 201 convention work.

What are CryptoProcessing webhooks?

CryptoProcessing calls them callbacks. You set one callback URL in the merchant portal, and every transaction kind is sent there: deposits, withdrawals, exchanges, payment requests and legacy invoices. A callback fires whenever a transaction's status changes, so a single deposit typically produces several callbacks as it moves from not_confirmed to confirmed. The body's type tells you the kind and status the new state.

Callbacks are signed with the secret of your account's active Master API key. X-Processing-Signature holds a hex HMAC-SHA512 of the raw body, and X-Processing-Key names the Master key that signed it.

CryptoProcessing webhook features

FeatureDetails
ConfigurationMerchant portal: one callback URL, plus Configure callbacks for optional ones
URL rulesHTTPS, on a domain that scanners such as VirusTotal consider safe
Signaturehex HMAC-SHA512
Signature headerx-processing-signature
Key headerX-Processing-Key, the public part of the signing Master key
Signed contentThe exact callback body
SecretThe secret of the active Master API key
BodyJSON
Event routingBody field type, then status
SuccessOnly HTTP 200 stops delivery
Retries12 after the first attempt, over about 24 hours. Not configurable
DuplicatesAnswer 201 to stop retries of a callback you already processed
Source IPsAvailable on request from Support

Events and payloads

typeTriggered WhenCommon Use Cases
depositA deposit to one of your addresses changes statusCredit user balances
withdrawalA withdrawal changes statusSettle or fail a payout
exchangeA currency conversion changes statusUpdate conversion records
payment_requestA payment request changes statusFulfil orders
deposit_exchangeA deposit that is converted on arrival changes statusCredit the converted amount
deposit_wallet_clientA wallet client deposit changes statusWallet top-ups
withdrawal_exchangeA withdrawal funded by a conversion changes statusPayouts in another currency
withdrawal_instantAn instant withdrawal changes statusFast payouts
withdrawal_instant_exchangeAn instant withdrawal funded by a conversion changes statusFast payouts in another currency
invoiceA legacy invoice changes statusOlder integrations

status values include confirmed, not_confirmed, cancelled, pending, paid, expired and failed; the set differs per kind (a payment_request callback shows processing, for example). Check the per-kind page of the docs for the full list.

Event Payload Structure

  • id identifies the transaction; payment_request callbacks use payment_request_id instead.
  • foreign_id is the id you supplied when creating the address, withdrawal or request.
  • currency_sent, currency_received, transactions and fees carry the amounts, as decimal strings.
  • Other kinds add their own fields (for example expires_at on payment_request and invoice).

deposit_wallet_client is only sent if business wallet transaction callbacks are enabled, and callbacks for transactions started in the portal are off by default. Switch on what you need under Configure callbacks.

Published payload example

This is the deposit sample from the webhook samples library. It is the example from CryptoProcessing's deposit callback reference.

{
  "id": 1,
  "end_user_reference": "user_12345",
  "type": "deposit",
  "crypto_address": {
    "id": 1,
    "currency": "USDC",
    "address": "0x9F2E4b7A1C3D5F6E8B0A2C4D6E8F1A3B5C7D9E0F",
    "tag": null,
    "foreign_id": "12345"
  },
  "currency_sent": {
    "currency": "USDC",
    "amount": "1000.00000000"
  },
  "currency_received": {
    "currency": "USDC",
    "amount": "1000.00000000",
    "amount_minus_fee": "997.00000000"
  },
  "transactions": [
    {
      "id": 1,
      "currency": "USDC",
      "transaction_type": "blockchain",
      "type": "deposit",
      "address": "0x9F2E4b7A1C3D5F6E8B0A2C4D6E8F1A3B5C7D9E0F",
      "tag": null,
      "amount": "1000.00000000",
      "txid": "0x3c8f1a92e6d4b7c0a5f2e8d1b6c9a4f7e0d3b8c5a1f6e9d2b4c7a0f3e8d5b1c9",
      "riskscore": "0.5",
      "confirmations": 3,
      "sender": {
        "address": "0x1D3F5A7C9E0B2D4F6A8C0E2B4D6F8A1C3E5B7D90"
      }
    }
  ],
  "fees": [
    {
      "type": "deposit",
      "currency": "USDC",
      "amount": "3.00000000"
    }
  ],
  "error": "",
  "status": "confirmed"
}

Setting up CryptoProcessing webhooks

Find the signing secret

Callbacks are signed with the secret of your active Master API key.

Paste the secret key of your active Master API key. Rotating that key changes the callback secret.

  1. Open the CryptoProcessing merchant portal and find your API keys.
  2. Identify your active Master key. Only the Master key signs callbacks; custom keys never do.
  3. Copy its secret key and store it as WEBHOOK_SECRET. Use it as-is: it is not hex- or base64-decoded.

The same secret signs your API requests, so whoever holds it can call the API as well as forge callbacks. Store it accordingly.

Set the callback URL

  1. In the merchant portal, set the callback URL, for example https://your-app.example.com/webhooks/cryptoprocessing.
  2. Under Configure callbacks, enable any optional callbacks you need.
  3. Route on the body's type in your handler, since everything arrives at this one URL.

To try it before deploying, forward callbacks to your machine with the Hookdeck CLI:

npx hookdeck-cli listen 3000 cryptoprocessing --path /webhooks/cryptoprocessing

Securing CryptoProcessing webhooks

Compute an HMAC-SHA512 of the raw body with the Master key's secret, encode it as hex and compare it with X-Processing-Signature in constant time. The result is 128 hex characters with no prefix. CryptoProcessing documents the hex encoding on its API authentication page, which uses the same scheme for signing requests. See the verification documentation.

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-processing-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha512', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{128}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

The handler verifies, then applies each status change once. A repeat of a callback it has already applied gets 201, which is CryptoProcessing's signal to stop retrying it.

const express = require('express');

function createCryptoProcessingApp({ applyStatus }) {
  const app = express();
  app.post('/webhooks/cryptoprocessing', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    let callback;
    try {
      callback = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!callback || typeof callback !== 'object' || Array.isArray(callback)) {
      return res.sendStatus(400);
    }
    if (typeof callback.type !== 'string' || callback.status === undefined) {
      return res.sendStatus(400);
    }
    const txId = callback.type === 'payment_request' ? callback.payment_request_id : callback.id;
    try {
      // Returns false if this (type, txId, status) was already applied.
      const applied = await applyStatus(callback.type, txId, callback.status, callback);
      return res.sendStatus(applied ? 200 : 201);
    } catch {
      return res.sendStatus(503);
    }
  });
  return app;
}

No timestamp is part of the signature, so dedupe on type, the transaction ID and status rather than on time.

Hookdeck's CryptoProcessing source type verifies X-Processing-Signature as a hex HMAC-SHA512 of the raw body with your Master key's secret, comparing case-insensitively. Hookdeck answers each callback with 200, which stops CryptoProcessing's retries, records a failed check as failed verification and does not forward that request. It has no timestamp window.

Make CryptoProcessing webhooks production-ready. Hookdeck Event Gateway answers each callback with the 200 CryptoProcessing needs, verifies the HMAC-SHA512 signature, and keeps every deposit and withdrawal status change until your ledger has applied it.

CryptoProcessing webhook limitations and pain points

Rotating the Master key changes the callback secret

The Problem: After a key rotation, every callback fails verification.

Why It Happens: Callbacks are signed with the active Master API key's secret. Activating a different Master key, for example as part of routine API key hygiene, changes the signing secret immediately.

Workarounds:

  • Treat Master key rotation as a webhook change too, and update WEBHOOK_SECRET at the same time.
  • Log X-Processing-Key so you can see which key signed each callback.

How Hookdeck Can Help: Update the secret on the Hookdeck source when you rotate. Hookdeck's request log shows any callbacks that failed verification in between, ready to replay.

Only 200 counts as success

The Problem: Callbacks keep arriving for a day even though your handler "succeeded".

Why It Happens: CryptoProcessing keeps a callback in its queue until it gets exactly 200. A 202 or 204 triggers the full retry schedule. For duplicates, it asks for 201 instead.

Workarounds:

  • Return 200 for a newly processed callback and 201 for a duplicate, and nothing else on success.
  • Answer quickly, because no timeout is documented.

How Hookdeck Can Help: Hookdeck always answers CryptoProcessing with 200, so its retries stop as soon as the callback is stored, and your service's status codes only affect Hookdeck's own retries.

One URL for everything

The Problem: Your deposit service receives withdrawal and exchange callbacks it has no business handling.

Why It Happens: CryptoProcessing has a single callback URL per account.

Workarounds:

  • Route on type in one handler and hand off to the right service.

How Hookdeck Can Help: Hookdeck filters on type can send deposits, withdrawals and exchanges to different destinations from one source.

Status changes, not events

The Problem: A deposit is credited twice, or a confirmed is overwritten by a late not_confirmed.

Why It Happens: Each status change is a separate callback, retries resend the same one, and the retry schedule spans about a day, so an older status can arrive late.

Workarounds:

  • Key processing on type, transaction ID and status.
  • Treat confirmed, cancelled and failed as terminal and ignore anything that would move a transaction backwards.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeats of the same transaction and status before they reach your ledger.

Best practices

Verify the raw body

Use express.raw({ type: '*/*' }) in Express, request.arrayBuffer() in Next.js or await request.body() in FastAPI, and don't gate the parser on application/json. Require exactly 128 hex characters and compare case-insensitively.

Use the right key

Verify with the secret of the active Master key, never a custom key and never the public value from X-Processing-Key.

Treat amounts as strings

currency_sent, currency_received and fees carry decimal strings. Use a decimal library rather than floating point when crediting balances.

Match on foreign_id

foreign_id is the ID you supplied when you created the address, withdrawal or request. It is the most reliable way to tie a callback to your own record.

Ask Support for the IP list

CryptoProcessing gives out callback IPs only on request. If you need an allowlist, request it from Support and keep signature verification as the primary check.

Conclusion

CryptoProcessing callbacks report every status change of every crypto transaction, signed with your Master key and retried for about a day until they get a 200. Verify with the active Master key's secret, answer 200 or 201 deliberately, and apply each status once in the right order.

Hookdeck Event Gateway takes the callback, satisfies CryptoProcessing's 200 rule, and delivers each status change to your ledger with retries you control.

Get started with Hookdeck for free and handle CryptoProcessing webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.