> ## 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.

# Partner Callback URLs

> Configure callback URLs to receive real-time transaction notifications

# Partner Callback URLs

Callback URLs (webhooks) are the **recommended integration method** for tracking transactions. Instead of polling the API, your server receives HTTP POST requests whenever transaction events occur.

```mermaid theme={null}
sequenceDiagram
    participant NowRamp as NowRamp API
    participant Queue as Webhook Queue
    participant Partner as Partner Server

    NowRamp->>Queue: Event occurs (transaction.completed)
    Queue->>Partner: POST /webhooks/ramp
    Partner-->>Queue: 200 OK
    Queue-->>NowRamp: Delivery confirmed

    Note over Queue,Partner: If delivery fails, retry with backoff
```

## Why Use Callback URLs?

<CardGroup cols={2}>
  <Card title="Real-Time Updates" icon="bolt">
    Receive notifications instantly when transactions complete, fail, or are refunded
  </Card>

  <Card title="Reduced API Calls" icon="arrow-down">
    No need to poll for status updates — events are pushed to you
  </Card>

  <Card title="Reliable Delivery" icon="rotate">
    Automatic retries with exponential backoff ensure delivery
  </Card>

  <Card title="Secure" icon="shield-check">
    HMAC signatures verify authenticity of every request
  </Card>
</CardGroup>

## Setup

### 1. Configure Your Endpoint

Configure your webhook URL through the **Partner Dashboard** at [dashboard.nowramp.com](https://dashboard.nowramp.com) under **Settings > Webhooks**.

### 2. Save Your Signing Key

When you set a webhook URL, a signing key is generated. Copy it from the Partner Dashboard under **Settings > Webhooks**.

<Warning>
  Store the signing key securely. It's used to verify webhook authenticity.
</Warning>

### 3. Implement Your Endpoint

Your endpoint must:

* Accept POST requests
* Return 2xx status within 30 seconds
* Verify the signature (strongly recommended)

## Request Format

Every callback includes these headers:

| Header                  | Description                                |
| ----------------------- | ------------------------------------------ |
| `Content-Type`          | `application/json`                         |
| `X-Webhook-Timestamp`   | Unix timestamp (seconds)                   |
| `X-Webhook-Signature`   | HMAC-SHA256 signature                      |
| `X-Webhook-Event`       | Event type (e.g., `transaction.completed`) |
| `X-Webhook-Delivery-Id` | Unique delivery ID                         |

### Payload 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"
        }
      },
      "createdAt": "2025-01-15T10:25:00.000Z",
      "completedAt": "2025-01-15T10:30:00.000Z"
    }
  }
}
```

<Info>
  The `metadata.partnerMetadata` object contains any custom data you passed via session `metadata` or checkout-intent `partnerMetadata`. Use it to correlate webhook events back to your internal orders. See the [Webhooks guide](/guides/webhooks#partner-metadata-in-webhooks) for details.
</Info>

## Signature Verification

**Always verify signatures** to ensure requests are from NowRamp.

### Algorithm

```
signature = HMAC-SHA256(
  key: your_signing_key,
  message: "{timestamp}.{json_payload}"
)
```

### Node.js Implementation

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(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 signaturePayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signaturePayload)
    .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 (!verifyWebhookSignature(payload, signature, timestamp, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(payload);
  processWebhookAsync(event);
  res.status(200).json({ received: true });
});
```

### Python Implementation

```python theme={null}
import hmac
import hashlib
import time

def verify_webhook_signature(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)
```

## Event Types

| Event                    | Trigger                                      |
| ------------------------ | -------------------------------------------- |
| `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                             |

### Example: transaction.completed

```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",
      "previousStatus": "processing",
      "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"
        }
      },
      "createdAt": "2025-01-15T10:25:00.000Z",
      "completedAt": "2025-01-15T10:30:00.000Z"
    }
  }
}
```

### Example: transaction.failed

```json theme={null}
{
  "id": "evt_txn_def456",
  "type": "transaction.failed",
  "timestamp": "2025-01-15T10:28:00.000Z",
  "data": {
    "order": {
      "id": "txn_abc789",
      "type": "buy",
      "status": "failed",
      "previousStatus": "processing",
      "source": {
        "currency": "EUR",
        "amount": "50.00"
      },
      "destination": {
        "currency": "USDC",
        "amount": "0",
        "address": "0x123..."
      },
      "externalOrderId": "gw_order_789",
      "customerId": "cust_uuid",
      "externalCustomerId": "user_456",
      "metadata": {
        "provider": "provider_b",
        "providerOrderId": "gw_order_789",
        "partnerMetadata": {
          "orderId": "your-internal-order-456"
        }
      },
      "createdAt": "2025-01-15T10:25:00.000Z"
    }
  }
}
```

## Retry Policy

Failed deliveries are automatically retried with exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 1 minute   |
| 3       | 5 minutes  |
| 4       | 15 minutes |
| 5       | 1 hour     |

After 5 failed attempts, the webhook is marked as failed and moved to the dead-letter queue.

## Webhook Delivery Management

NowRamp provides partner-facing endpoints to monitor and manage webhook deliveries.

### Delivery Statuses

| Status      | Description                                                    |
| ----------- | -------------------------------------------------------------- |
| `pending`   | Awaiting delivery — queued and will be sent shortly            |
| `delivered` | Successfully delivered — your endpoint returned a 2xx response |
| `failed`    | Delivery failed after retries — moved to dead-letter queue     |

### List Webhook Deliveries

```bash theme={null}
curl "https://api.nowramp.com/v1/webhooks/events?status=failed" \
  -H "X-API-Key: sk_live_your_secret_key"
```

**Query Parameters:**

| Parameter   | Type   | Description                                          |
| ----------- | ------ | ---------------------------------------------------- |
| `status`    | string | Filter by status: `pending`, `delivered`, `failed`   |
| `eventType` | string | Filter by event type (e.g., `transaction.completed`) |
| `limit`     | number | Results per page (default: 50, max: 100)             |
| `offset`    | number | Pagination offset                                    |

### Retry Failed Delivery

```bash theme={null}
curl -X POST "https://api.nowramp.com/v1/webhooks/events/del_def456/retry" \
  -H "X-API-Key: sk_live_your_secret_key"
```

<Note>
  You can only retry deliveries with `failed` status.
</Note>

### Partner Dashboard

You can also manage webhook deliveries from the [Partner Dashboard](https://dashboard.nowramp.com):

* View delivery history with filtering
* Inspect full payload and response details
* Retry failed deliveries with one click
* Monitor delivery health and success rates

## Best Practices

### 1. Respond Quickly

Return a 2xx response as fast as possible. Process events asynchronously:

```javascript theme={null}
app.post('/webhooks/ramp', (req, res) => {
  res.status(200).json({ received: true });
  queue.add('process-webhook', req.body);
});
```

### 2. Handle Duplicates

Events may be delivered more than once. Use the event `id` for idempotency:

```javascript theme={null}
async function processWebhook(event) {
  const existing = await db.webhookEvents.findOne({ eventId: event.id });
  if (existing) return;

  await handleEvent(event);
  await db.webhookEvents.insert({ eventId: event.id, processedAt: new Date() });
}
```

### 3. Use HTTPS

Webhook endpoints must use HTTPS in production.

### 4. Verify Signatures

Always verify the `X-Webhook-Signature` header.

### 5. Monitor Failures

Set up alerts for failed webhook deliveries. Use the delivery management API or Partner Dashboard to monitor delivery health.

## Testing

### Sandbox Events

In sandbox mode, trigger test events:

```bash theme={null}
curl -X POST https://api.sandbox.nowramp.com/v1/webhooks/test \
  -H "X-API-Key: sk_test_your_secret_key" \
  -d '{"eventType": "transaction.completed"}'
```

### Local Development

Use ngrok or similar to expose your local server:

```bash theme={null}
ngrok http 3000
# Use the https URL as your webhook URL
```

## Troubleshooting

| Issue                | Solution                                         |
| -------------------- | ------------------------------------------------ |
| No webhooks received | Check webhook URL is set in project settings     |
| Signature invalid    | Verify using raw request body, check signing key |
| Events delayed       | Check your endpoint returns 2xx quickly          |
| Missing events       | Check dead-letter queue for failed deliveries    |
