> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nowramp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications for transaction events

# 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](https://dashboard.nowramp.com)
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

```bash theme={null}
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:

```json theme={null}
{
  "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

```javascript theme={null}
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

```python theme={null}
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                             |

## 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`:

```json theme={null}
{
  "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 |

<Info>
  System keys (`provider`, `providerOrderId`, `email`, `paymentMethodId`) are reserved and cannot be overwritten by partner data. Your data is always safely namespaced under `partnerMetadata`.
</Info>

### How metadata flows

| 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"` |

### Extracting your data from a webhook

```javascript theme={null}
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

```javascript theme={null}
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:

```bash theme={null}
# 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:

```javascript theme={null}
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:

```javascript theme={null}
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:

```javascript theme={null}
function handleWebhook(event) {
  console.log('Webhook received:', {
    id: event.id,
    type: event.type,
    timestamp: event.timestamp,
  });
}
```
