Guide to CryptoProcessing Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | Merchant portal: one callback URL, plus Configure callbacks for optional ones |
| URL rules | HTTPS, on a domain that scanners such as VirusTotal consider safe |
| Signature | hex HMAC-SHA512 |
| Signature header | x-processing-signature |
| Key header | X-Processing-Key, the public part of the signing Master key |
| Signed content | The exact callback body |
| Secret | The secret of the active Master API key |
| Body | JSON |
| Event routing | Body field type, then status |
| Success | Only HTTP 200 stops delivery |
| Retries | 12 after the first attempt, over about 24 hours. Not configurable |
| Duplicates | Answer 201 to stop retries of a callback you already processed |
| Source IPs | Available on request from Support |
Events and payloads
type | Triggered When | Common Use Cases |
|---|---|---|
deposit | A deposit to one of your addresses changes status | Credit user balances |
withdrawal | A withdrawal changes status | Settle or fail a payout |
exchange | A currency conversion changes status | Update conversion records |
payment_request | A payment request changes status | Fulfil orders |
deposit_exchange | A deposit that is converted on arrival changes status | Credit the converted amount |
deposit_wallet_client | A wallet client deposit changes status | Wallet top-ups |
withdrawal_exchange | A withdrawal funded by a conversion changes status | Payouts in another currency |
withdrawal_instant | An instant withdrawal changes status | Fast payouts |
withdrawal_ | An instant withdrawal funded by a conversion changes status | Fast payouts in another currency |
invoice | A legacy invoice changes status | Older 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
ididentifies the transaction;payment_requestcallbacks usepayment_request_idinstead.foreign_idis the id you supplied when creating the address, withdrawal or request.currency_sent,currency_received,transactionsandfeescarry the amounts, as decimal strings.- Other kinds add their own fields (for example
expires_atonpayment_requestandinvoice).
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.
- Open the CryptoProcessing merchant portal and find your API keys.
- Identify your active Master key. Only the Master key signs callbacks; custom keys never do.
- 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
- In the merchant portal, set the callback URL, for example
https://your-app.example.com/webhooks/cryptoprocessing. - Under Configure callbacks, enable any optional callbacks you need.
- Route on the body's
typein 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
200CryptoProcessing 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_SECRETat the same time. - Log
X-Processing-Keyso 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
200for a newly processed callback and201for 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
typein 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 andstatus. - Treat
confirmed,cancelledandfailedas 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.