Webhooks
Webhooks notify your server in real-time when transaction events occur — completions, failures, refunds, and more.
Setting Up Webhooks
Via Dashboard
- Go to Settings > Webhooks in the Partner Dashboard
- Click Add Endpoint
- Enter your endpoint URL
- Select the events you want to receive
- Save and note the signing secret
Via API
curl -X POST https://api.nowramp.com/v1/webhooks \
-H "X-API-Key: sk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/ramp",
"events": ["transaction.completed", "transaction.failed"],
"secret": "your_signing_secret"
}'
Webhook Payload
All webhooks follow this structure:
{
"id": "evt_txn_abc123",
"type": "transaction.completed",
"timestamp": "2025-01-15T10:30:00.000Z",
"data": {
"order": {
"id": "txn_xyz789",
"type": "buy",
"status": "completed",
"source": {
"currency": "USD",
"amount": "100.00"
},
"destination": {
"currency": "ETH",
"amount": "0.0523",
"address": "0x742d35Cc6634C0532925a3b844Bc9e7595f1bD21"
},
"externalOrderId": "gw_order_456",
"customerId": "cust_uuid",
"externalCustomerId": "user_123",
"metadata": {
"provider": "provider_a",
"providerOrderId": "gw_order_456",
"email": "user@example.com",
"partnerMetadata": {
"orderId": "your-internal-order-123",
"campaign": "summer2026"
}
},
"createdAt": "2025-01-15T10:25:00.000Z",
"completedAt": "2025-01-15T10:30:00.000Z"
}
}
}
Verifying Signatures
All webhooks include an HMAC signature in the X-Webhook-Signature header. Always verify this signature.
Node.js
const crypto = require('crypto');
function verifyWebhook(payload, signature, timestamp, secret) {
// Check timestamp is recent (within 5 minutes)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return false;
}
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch {
return false;
}
}
// Express middleware
app.post('/webhooks/ramp', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = req.body.toString();
if (!verifyWebhook(payload, signature, timestamp, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
handleWebhookEvent(event);
res.status(200).send('OK');
});
Python
import hmac
import hashlib
import time
def verify_webhook(payload: str, signature: str, timestamp: str, secret: str) -> bool:
now = int(time.time())
if abs(now - int(timestamp)) > 300:
return False
signature_payload = f"{timestamp}.{payload}"
expected = hmac.new(
secret.encode(),
signature_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Transaction Events
| Event | Description |
|---|
transaction.pending | Checkout created, awaiting payment |
transaction.processing | Payment received, processing crypto transfer |
transaction.completed | Crypto delivered to wallet |
transaction.failed | Transaction failed |
transaction.cancelled | Transaction cancelled by user or system |
transaction.refunded | Payment refunded |
Every webhook payload includes a metadata object on the order. If you passed partnerMetadata when creating a checkout intent, or metadata when creating a session, it appears under metadata.partnerMetadata:
{
"metadata": {
"provider": "banxa",
"providerOrderId": "ext-order-abc",
"email": "user@example.com",
"paymentMethodId": "debit-credit-card",
"partnerMetadata": {
"orderId": "your-internal-order-123",
"userId": "player_456"
}
}
}
| Key | Description |
|---|
provider | Payment gateway used (system-managed) |
providerOrderId | Gateway’s order reference (system-managed) |
email | Customer email (system-managed) |
paymentMethodId | Payment method used (system-managed) |
partnerMetadata | Your custom data, passed through unchanged |
System keys (provider, providerOrderId, email, paymentMethodId) are reserved and cannot be overwritten by partner data. Your data is always safely namespaced under partnerMetadata.
| Entry point | You send | Stored as |
|---|
POST /onramp/checkout-intent with partnerMetadata | { "orderId": "abc" } | order.metadata.partnerMetadata.orderId = "abc" |
POST /v1/sessions with metadata | { "orderId": "abc" } | Session → order: order.metadata.partnerMetadata.orderId = "abc" |
app.post('/webhooks/ramp', (req, res) => {
const event = req.body;
const order = event.data.order;
const partnerData = order.metadata?.partnerMetadata;
if (partnerData?.orderId) {
// Match back to your internal order
await db.orders.update(partnerData.orderId, {
rampOrderId: order.id,
status: order.status,
});
}
res.status(200).json({ received: true });
});
Handling Events
function handleWebhookEvent(event) {
switch (event.type) {
case 'transaction.completed':
handleTransactionCompleted(event.data.transaction);
break;
case 'transaction.failed':
handleTransactionFailed(event.data.transaction);
break;
case 'transaction.refunded':
handleTransactionRefunded(event.data.transaction);
break;
default:
console.log('Unhandled event type:', event.type);
}
}
function handleTransactionCompleted(transaction) {
// Update your database
db.transactions.update(transaction.id, { status: 'completed' });
// Notify the user
sendNotification(transaction.customerId, 'Your crypto purchase is complete!');
}
Retry Policy
Failed webhook deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
| 7 | 24 hours |
After 7 failed attempts, the webhook is moved to the dead-letter queue.
Dead-Letter Queue
Failed webhooks are stored in the dead-letter queue for manual review:
# View failed webhooks
curl https://api.nowramp.com/v1/webhooks/dead-letter \
-H "X-API-Key: sk_live_your_secret_key"
# Retry a failed webhook
curl -X POST https://api.nowramp.com/v1/webhooks/dead-letter/evt_abc123/retry \
-H "X-API-Key: sk_live_your_secret_key"
Best Practices
1. Respond Quickly
Return a 2xx response as soon as possible. Process the event asynchronously:
app.post('/webhooks/ramp', (req, res) => {
res.status(200).send('OK');
processWebhookAsync(req.body);
});
2. Handle Duplicates
Webhooks may be delivered more than once. Use idempotency:
async function handleWebhook(event) {
const processed = await db.webhookEvents.findOne({ id: event.id });
if (processed) return;
await processEvent(event);
await db.webhookEvents.insert({ id: event.id, processedAt: new Date() });
}
3. Verify Signatures
Always verify the webhook signature before processing.
4. Use HTTPS
Webhook endpoints must use HTTPS in production.
5. Log Everything
Log webhook events for debugging:
function handleWebhook(event) {
console.log('Webhook received:', {
id: event.id,
type: event.type,
timestamp: event.timestamp,
});
}