Skip to main content

Webhooks

Webhooks notify your server in real-time when transaction events occur — completions, failures, refunds, and more.

Setting Up Webhooks

Via Dashboard

  1. Go to Settings > Webhooks in the Partner Dashboard
  2. Click Add Endpoint
  3. Enter your endpoint URL
  4. Select the events you want to receive
  5. 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

EventDescription
transaction.pendingCheckout created, awaiting payment
transaction.processingPayment received, processing crypto transfer
transaction.completedCrypto delivered to wallet
transaction.failedTransaction failed
transaction.cancelledTransaction cancelled by user or system
transaction.refundedPayment refunded

Partner Metadata in Webhooks

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"
    }
  }
}
KeyDescription
providerPayment gateway used (system-managed)
providerOrderIdGateway’s order reference (system-managed)
emailCustomer email (system-managed)
paymentMethodIdPayment method used (system-managed)
partnerMetadataYour 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.

How metadata flows

Entry pointYou sendStored 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"

Extracting your data from a webhook

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:
AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
68 hours
724 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,
  });
}