Webhooks
Webhooks allow your application to receive real-time notifications when events occur in your crypto payment infrastructure. Get instant alerts for deposits, withdrawals, balance changes, and more.
Registering webhooks
Configure your webhook URL using the organization endpoint to receive event notifications:
Set webhook URL
curl -X PATCH https://api.coinspayd.io/organisation/webhook \
-H "x-api-key: {your-api-key}" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://your-app.com/webhooks/coinspayd"
}'
Once configured, the platform will send HTTP POST requests to your webhook URL whenever relevant events occur.
Consuming webhooks
Your webhook endpoint should:
- Accept HTTP POST requests
- Parse the JSON payload
- Verify the signature (see Security)
- Process the event based on the
typefield - Return a 2xx status code to acknowledge receipt
Example webhook handler
app.post('/webhooks/coinspayd', async (req, res) => {
const { type, payload } = req.body
// Verify signature (see Security section)
if (!verifySignature(req)) {
return res.status(401).send('Invalid signature')
}
switch (type) {
case 'deposit.detected':
await handleNewDeposit(payload)
break
case 'withdrawal.completed':
await handleWithdrawalComplete(payload)
break
case 'withdrawal.failed':
await handleWithdrawalFailed(payload)
break
default:
console.log(`Unknown event type: ${type}`)
}
res.status(200).send('OK')
})
Event types
Deposit events
- Name
deposit.detected- Description
A new cryptocurrency deposit was detected on the blockchain. This is sent as soon as the transaction is seen, before confirmations.
- Name
deposit.confirmed- Description
A deposit has received sufficient blockchain confirmations and has been credited to the user's account.
Withdrawal events
- Name
withdrawal.created- Description
A new withdrawal request was created and is pending approval or processing.
- Name
withdrawal.approved- Description
A withdrawal has received the required number of approvals and will begin processing.
- Name
withdrawal.processing- Description
A withdrawal transaction has been broadcast to the blockchain.
- Name
withdrawal.completed- Description
A withdrawal transaction has been confirmed on the blockchain.
- Name
withdrawal.failed- Description
A withdrawal failed due to insufficient gas, network issues, or other errors.
Account events
- Name
account.created- Description
A new deposit account (address) was created for a user.
- Name
balance.updated- Description
An account balance changed due to a deposit, withdrawal, or sweep.
deposit.detected
{
"type": "deposit.detected",
"timestamp": "2025-01-30T10:30:00.000Z",
"payload": {
"chainId": "eip155:1",
"txnHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"tokenId": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"amount": "100000000",
"externalUserId": "customer_789",
"accountChainId": "eip155:1",
"senderAddress": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
"blockNumber": 18500000,
"confirmations": 0,
"Token": {
"id": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"name": "USD Coin",
"symbol": "USDC",
"decimals": 6,
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
}
}
}
withdrawal.completed
{
"type": "withdrawal.completed",
"timestamp": "2025-01-30T11:00:00.000Z",
"payload": {
"id": "withdrawal_abc123",
"chainId": "eip155:137",
"tokenId": "eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
"amount": "50000000",
"toAddress": "0x9876543210987654321098765432109876543210",
"txnHash": "0xfedcbafedcbafedcbafedcbafedcbafedcbafedcbafedcbafedcbafedcbafed",
"status": "Completed",
"externalUserId": "customer_456",
"approvedBy": ["user_001", "user_002"],
"createdAt": "2025-01-30T10:45:00.000Z",
"completedAt": "2025-01-30T11:00:00.000Z",
"Token": {
"id": "eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
"name": "USD Coin",
"symbol": "USDC",
"decimals": 6,
"address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174"
}
}
}
account.created
{
"type": "account.created",
"timestamp": "2025-01-30T09:00:00.000Z",
"payload": {
"orgId": "org_456",
"chainId": "xrpl:mainnet",
"externalUserId": "customer_999",
"orgDepositAccountId": "acc_xyz789",
"properties": {
"address": "rN7n7otQDd6FczFgLdlqtyMVrn3HMtthca",
"destinationTag": "54321"
},
"createdAt": "2025-01-30T09:00:00.000Z"
}
}
Security
To verify that a webhook was genuinely sent by the Crypto Deposits Platform and not by a malicious actor, you should verify the request signature. Each webhook request includes an x-webhook-signature header containing an HMAC SHA-256 hash of the request payload.
Getting your webhook secret
Your webhook secret is provided when you configure your webhook URL. Keep this secret secure and never commit it to version control.
Verifying signatures
Verifying webhook signatures
const crypto = require('crypto')
function verifyWebhookSignature(req, secret) {
const signature = req.headers['x-webhook-signature']
const payload = JSON.stringify(req.body)
const hash = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
return hash === signature
}
// Usage in Express
app.post('/webhooks/coinspayd', (req, res) => {
if (!verifyWebhookSignature(req, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' })
}
// Process webhook
const { type, payload } = req.body
// ...
res.status(200).send('OK')
})
Important security practices:
- Always verify signatures before processing webhook data
- Use constant-time comparison functions to prevent timing attacks
- Store webhook secrets in environment variables or secrets managers
- Use HTTPS endpoints to prevent man-in-the-middle attacks
- Implement rate limiting to prevent abuse
Retry logic
If your webhook endpoint returns a non-2xx status code or times out, the platform will retry delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 5 seconds |
| 2nd retry | 25 seconds |
| 3rd retry | 2 minutes |
| 4th retry | 10 minutes |
| 5th retry | 1 hour |
After 5 failed attempts, the webhook is marked as failed and will not be retried. You can view failed webhooks in your dashboard.
Testing webhooks
Local development
Use a tool like ngrok to expose your local server for webhook testing:
# Start ngrok
ngrok http 3000
# Update your webhook URL to the ngrok URL
curl -X PATCH https://api.coinspayd.io/organisation/webhook \
-H "x-api-key: {your-api-key}" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://abc123.ngrok.io/webhooks/coinspayd"
}'
Manual testing
Trigger test deposits by sending crypto to a test deposit address on a testnet:
# Get a deposit address on Sepolia testnet
curl -G https://api.coinspayd.io/payments/deposit-address \
-H "x-api-key: {your-api-key}" \
-d chainId=eip155:11155111 \
-d tokenId=eip155:11155111/slip44:60 \
-d userId=test_user_001
Then send testnet ETH to the returned address and monitor your webhook endpoint for the deposit.detected event.
Best practices
Idempotency
Webhooks may be delivered more than once. Design your webhook handler to be idempotent by:
- Tracking processed webhook IDs in your database
- Using transaction hashes as idempotency keys
- Checking if an event was already processed before taking action
async function handleDeposit(payload) {
const { txnHash, externalUserId, amount } = payload
// Check if already processed
const existing = await db.deposits.findOne({ txnHash })
if (existing) {
console.log(`Deposit ${txnHash} already processed`)
return
}
// Process deposit
await db.deposits.create({
txnHash,
userId: externalUserId,
amount,
processedAt: new Date()
})
await creditUserAccount(externalUserId, amount)
}
Asynchronous processing
Process webhooks asynchronously to avoid timeouts:
app.post('/webhooks/coinspayd', async (req, res) => {
// Verify signature
if (!verifyWebhookSignature(req, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature')
}
// Acknowledge immediately
res.status(200).send('OK')
// Process asynchronously
const { type, payload } = req.body
queue.add('process-webhook', { type, payload })
})
Monitoring
Monitor webhook delivery:
- Track webhook delivery success rates
- Alert on repeated failures
- Log webhook processing times
- Monitor for unusual patterns (e.g., duplicate events)
Error handling
Handle errors gracefully:
app.post('/webhooks/coinspayd', async (req, res) => {
try {
if (!verifyWebhookSignature(req, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature')
}
const { type, payload } = req.body
switch (type) {
case 'deposit.detected':
await handleDeposit(payload)
break
case 'withdrawal.completed':
await handleWithdrawalComplete(payload)
break
default:
console.log(`Unknown event type: ${type}`)
}
res.status(200).send('OK')
} catch (error) {
console.error('Webhook processing error:', error)
// Return 5xx for retryable errors, 4xx for permanent failures
if (error.retryable) {
res.status(503).send('Service temporarily unavailable')
} else {
res.status(400).send('Bad request')
}
}
})