Webhooks
Receive real-time notifications when invoice statuses change or withdrawals are processed. All webhooks are signed with HMAC-SHA256 for security.
Available Events
Invoice Events
invoice.createdNew invoice created
invoice.detectedPayment detected on blockchain (0 confirmations)
invoice.partialPartial payment received (less than expected amount)
invoice.overpaidOverpayment received (more than expected amount)
invoice.confirmedPayment confirmed with required block confirmations
invoice.processing_payoutSettlement in progress (sweep/split started)
invoice.paid_outFunds distributed to fee/custody wallets and credited to balance
invoice.expiredInvoice expired without full payment
invoice.failedSettlement failed
invoice.late_paymentPayment received after invoice expiration
Withdrawal Events
withdrawal.createdNew withdrawal request created
withdrawal.completedWithdrawal processed successfully
withdrawal.failedWithdrawal failed
Order Events (Storefront)
order.createdNew order placed
order.paidOrder payment confirmed
order.fulfilledOrder marked as fulfilled
order.cancelledOrder cancelled
Create Webhook Endpoint
/v1/webhooks/endpointsRegister a new webhook endpoint for your tenant
const response = await fetch('https://api.fromchain.plus/v1/webhooks/endpoints', {
method: 'POST',
headers: {
'Authorization': 'Bearer gw_live_...',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://yourdomain.com/api/webhooks',
enabled: true,
eventsSubscribed: [
'invoice.confirmed',
'invoice.paid_out',
'invoice.failed',
'withdrawal.created',
'withdrawal.completed',
'withdrawal.failed'
]
})
});
const endpoint = await response.json();
// {
// "id": "wh_endpoint_abc123",
// "url": "https://yourdomain.com/api/webhooks",
// "secret": "whsec_xyz789...", // Store this securely!
// "enabled": true,
// "eventsSubscribed": ["invoice.confirmed", "invoice.paid_out", "invoice.failed"],
// "createdAt": "2025-12-18T10:00:00Z"
// }Verifying Webhook Signatures
Every webhook includes HMAC-SHA256 signature headers for verification:
POST /api/webhooks HTTP/1.1
Host: yourdomain.com
Content-Type: application/json
X-Webhook-Id: evt_abc123
X-Webhook-Timestamp: 1702905600000
X-Webhook-Signature: v1=a1b2c3d4e5f6...
X-Tenant-Id: tenant_xyz789
{
"id": "evt_abc123",
"type": "invoice.partial",
"createdAt": "2025-12-18T11:00:00.000Z",
"data": {
"invoiceId": "inv_123",
"tenantId": "tenant_xyz",
"chain": "BSC",
"token": "USDT",
"depositAddress": "0x1234...5678",
"amountExpected": "100.00",
"amountReceived": "50.00",
"status": "PARTIALLY_PAID",
"expiresAt": "2025-12-18T11:30:00.000Z",
"confirmationsRequired": 5,
"txHashes": ["0xabc...def"],
"metadata": {}
}
}Verify the signature to ensure the webhook came from FromChain:
import crypto from 'crypto';
function verifyWebhook(
payload: string, // Raw request body
timestamp: string, // X-Webhook-Timestamp header
signature: string, // X-Webhook-Signature header
secret: string // Your endpoint secret
): boolean {
// Construct the signed payload
const signedPayload = `${timestamp}.${payload}`;
// Compute HMAC
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Compare signatures (constant-time comparison)
const receivedSignature = signature.replace('v1=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);
}
// Express.js example
app.post('/api/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const timestamp = req.headers['x-webhook-timestamp'] as string;
const payload = req.body.toString();
// Verify timestamp to prevent replay attacks
const now = Date.now();
const webhookTime = parseInt(timestamp);
if (Math.abs(now - webhookTime) > 5 * 60 * 1000) { // 5 minutes
return res.status(400).send('Timestamp too old');
}
// Verify signature
if (!verifyWebhook(payload, timestamp, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
const event = JSON.parse(payload);
console.log('Valid webhook received:', event.type);
res.status(200).send('OK');
});Retry Behavior
If your endpoint fails (non-2xx response), FromChain will retry with exponential backoff:
| Attempt | Delay | Total Time |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 30 seconds | 30s |
| 3 | 2 minutes | 2m 30s |
| 4 | 5 minutes | 7m 30s |
| 5 | 15 minutes | 22m 30s |
| 6 | 1 hour | 1h 22m |
| 7 | 3 hours | 4h 22m |
| 8 | 6 hours | 10h 22m |
⚠️ Important: After 8 failed attempts, the delivery moves to a dead-letter queue. You can manually replay failed deliveries via the API.
Webhook Management
/v1/webhooks/endpointsList all webhook endpoints
/v1/webhooks/endpoints/:idUpdate endpoint URL or subscribed events
/v1/webhooks/endpoints/:id/rotate-secretRotate the webhook secret
/v1/webhooks/endpoints/:id/testSend a test webhook event
/v1/webhooks/replay/:eventIdManually replay a specific webhook event
/v1/webhooks/deliveriesList webhook delivery attempts
Best Practices
✅ Always verify signatures
Never process webhooks without verifying the HMAC signature to prevent spoofing.
✅ Respond quickly
Return a 200 response immediately. Process the webhook asynchronously using a queue.
✅ Handle idempotency
Use the X-Webhook-Id header to deduplicate webhook deliveries.
✅ Use HTTPS endpoints
Webhook URLs must use HTTPS in production for security.
