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

# Webhooks

> Receive real-time notifications for ramp conversions, account updates, and transaction status

Webhooks provide real-time notifications about ramp operations, allowing you to respond immediately to conversion completions, account balance changes, and transaction status updates.

## Webhook events for ramps

Grid sends webhooks for key events in the ramp lifecycle:

### Conversion events

> [tab] OUTGOING\_PAYMENT
>
> Sent when a conversion (on-ramp or off-ramp) changes status. Webhook types use the format `OUTGOING_PAYMENT.<STATUS>` (e.g., `OUTGOING_PAYMENT.COMPLETED`, `OUTGOING_PAYMENT.FAILED`, `OUTGOING_PAYMENT.REFUND_COMPLETED`).
>
> ```json theme={null}
> {
>   "id": "Webhook:019542f5-b3e7-1d02-0000-000000000030",
>   "type": "OUTGOING_PAYMENT.COMPLETED",
>   "timestamp": "2025-10-03T15:03:00Z",
>   "data": {
>     "id": "Transaction:019542f5-b3e7-1d02-0000-000000000025",
>     "status": "COMPLETED",
>     "type": "OUTGOING",
>     "direction": "DEBIT",
>     "sentAmount": {
>       "amount": 10000,
>       "currency": { "code": "USD", "decimals": 2 }
>     },
>     "receivedAmount": {
>       "amount": 95000,
>       "currency": { "code": "BTC", "decimals": 8 }
>     },
>     "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
>     "settledAt": "2025-10-03T15:02:30Z",
>     "exchangeRate": 9.5,
>     "quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006",
>     "paymentRail": "ACH",
>     "railSelectionMode": "AUTO",
>     "expectedSettlementAt": "2025-10-04T15:00:00Z",
>     "settlementTimelineSeconds": 86400
>   }
> }
> ```
>
> > [!TIP]
> >
> > Use this webhook to update your UI, credit customer accounts, and trigger post-conversion workflows. See the [Transaction Lifecycle](https://agents-docs.lightspark.com/platform-overview/core-concepts/transaction-lifecycle.md) guide for all status transitions and refund handling.

> [tab] INTERNAL\_ACCOUNT.BALANCE\_UPDATED
>
> Sent when internal account balances change (deposits, conversions, withdrawals).
>
> ```json theme={null}
> {
>   "id": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
>   "type": "INTERNAL_ACCOUNT.BALANCE_UPDATED",
>   "timestamp": "2025-10-03T15:03:00Z",
>   "data": {
>     "id": "InternalAccount:btc456",
>     "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
>     "type": "INTERNAL_CRYPTO",
>     "status": "ACTIVE",
>     "balance": {
>       "amount": 5000000,
>       "currency": { "code": "BTC", "name": "Bitcoin", "symbol": "₿", "decimals": 8 }
>     },
>     "totalBalance": {
>       "amount": 5000000,
>       "currency": { "code": "BTC", "name": "Bitcoin", "symbol": "₿", "decimals": 8 }
>     },
>     "fundingPaymentInstructions": [],
>     "createdAt": "2025-08-01T10:00:00Z",
>     "updatedAt": "2025-10-03T15:03:00Z"
>   }
> }
> ```
>
> > [!NOTE]
> >
> > Critical for tracking crypto deposits (for off-ramps) and fiat balance changes.

> [tab] INTERNAL\_ACCOUNT.STATUS\_UPDATED
>
> Sent when the status of an internal account changes (e.g., PENDING → ACTIVE, ACTIVE → FROZEN).
>
> ```json theme={null}
> {
>   "id": "Webhook:019542f5-b3e7-1d02-0000-000000000008",
>   "type": "INTERNAL_ACCOUNT.STATUS_UPDATED",
>   "timestamp": "2025-10-03T15:03:00Z",
>   "data": {
>     "id": "InternalAccount:btc456",
>     "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
>     "type": "INTERNAL_CRYPTO",
>     "status": "FROZEN",
>     "balance": {
>       "amount": 5000000,
>       "currency": { "code": "BTC", "name": "Bitcoin", "symbol": "₿", "decimals": 8 }
>     },
>     "totalBalance": {
>       "amount": 5000000,
>       "currency": { "code": "BTC", "name": "Bitcoin", "symbol": "₿", "decimals": 8 }
>     },
>     "fundingPaymentInstructions": [],
>     "createdAt": "2025-08-01T10:00:00Z",
>     "updatedAt": "2025-10-03T15:03:00Z"
>   }
> }
> ```
>
> > [!WARNING]
> >
> > Account status values: `PENDING` (provisioning), `ACTIVE` (ready for payments), `CLOSED` (customer-initiated), `FROZEN` (compliance/fraud hold), `FAILED` (provisioning failed). Frozen and failed accounts cannot send or receive payments.

> [tab] CUSTOMER.KYC\_\*
>
> Sent when customer KYC verification completes (required before conversions). Business customers emit the `CUSTOMER.KYB_*` siblings.
>
> ```json theme={null}
> {
>   "id": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
>   "type": "CUSTOMER.KYC_APPROVED",
>   "timestamp": "2025-10-03T14:32:00Z",
>   "data": {
>     "id": "Customer:019542f5-b3e7-1d02-0000-000000000001",
>     "platformCustomerId": "user_12345",
>     "customerType": "INDIVIDUAL",
>     "kycStatus": "APPROVED",
>     "fullName": "Jane Doe",
>     "createdAt": "2025-07-21T17:32:28Z",
>     "updatedAt": "2025-10-03T14:32:00Z"
>   }
> }
> ```
>
> > [!TIP]
> >
> > Enable ramp access immediately when KYC status changes to `APPROVED`.

## Webhook configuration

Configure your webhook endpoint in the Grid dashboard or via API:

```bash theme={null}
curl -X PATCH 'https://api.lightspark.com/grid/2025-10-13/config' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "webhookEndpoint": "https://api.yourapp.com/webhooks/grid"
  }'
```

> [!WARNING]
>
> Your webhook endpoint must be publicly accessible over HTTPS and respond
> within 30 seconds.

## Webhook verification

Always verify webhook signatures to ensure authenticity:

### Verification steps

**1. Extract signature header**

Get the `X-Grid-Signature` header from the webhook request.

```javascript theme={null}
const signature = req.headers['x-grid-signature'];
```

**2. Get public key**

Retrieve the Grid public key from your dashboard (provided during onboarding).

```javascript theme={null}
const publicKey = process.env.GRID_PUBLIC_KEY;
```

**3. Verify signature**

Use the public key to verify the signature against the raw request body.

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

function verifyWebhookSignature(payload, signature, publicKey) {
  const verify = crypto.createVerify('SHA256');
  verify.update(payload);
  verify.end();
  
  return verify.verify(publicKey, signature, 'base64');
}

// In your webhook handler
const rawBody = JSON.stringify(req.body);
const isValid = verifyWebhookSignature(rawBody, signature, publicKey);

if (!isValid) {
  return res.status(401).json({ error: 'Invalid signature' });
}
```

> [!NOTE]
>
> The signature is created using secp256r1 (P-256) asymmetric cryptography with
> SHA-256 hashing.

## Handling ramp webhooks

### On-ramp completion

Handle successful fiat-to-crypto conversions:

```javascript theme={null}
app.post("/webhooks/grid", async (req, res) => {
  // Verify signature
  if (!verifySignature(req.body, req.headers["x-grid-signature"])) {
    return res.status(401).end();
  }

  const { type, data } = req.body;

  if (type === "OUTGOING_PAYMENT.COMPLETED") {
    // On-ramp completed (USD → BTC)
    if (
      data.sentAmount.currency.code === "USD" &&
      data.receivedAmount.currency.code === "BTC"
    ) {
      await db.transactions.create({
        userId: data.customerId,
        type: "ON_RAMP",
        amountUsd: data.sentAmount.amount,
        amountBtc: data.receivedAmount.amount,
        rate: data.exchangeRate,
        status: "COMPLETED",
        completedAt: new Date(data.settledAt),
      });

      await sendNotification(data.customerId, {
        title: "Bitcoin purchased!",
        message: `You received ${formatBtc(data.receivedAmount.amount)} BTC`,
      });
    }
  }

  res.status(200).json({ received: true });
});
```

### Off-ramp completion

Handle successful crypto-to-fiat conversions:

```javascript theme={null}
if (type === "OUTGOING_PAYMENT.COMPLETED") {
  // Off-ramp completed (BTC → USD)
  if (
    data.sentAmount.currency.code === "BTC" &&
    data.receivedAmount.currency.code === "USD"
  ) {
    await db.transactions.create({
      userId: data.customerId,
      type: "OFF_RAMP",
      amountBtc: data.sentAmount.amount,
      amountUsd: data.receivedAmount.amount,
      rate: data.exchangeRate,
      status: "COMPLETED",
      bankAccountId: data.destination.accountId,
      completedAt: new Date(data.settledAt),
    });

    await sendNotification(data.customerId, {
      title: "Cash out completed!",
      message: `$${formatUsd(data.receivedAmount.amount)} sent to your bank account`,
    });
  }
}
```

### Balance updates

Track crypto deposits for off-ramp liquidity:

```javascript theme={null}
if (type === "INTERNAL_ACCOUNT.BALANCE_UPDATED") {
  const { data } = req.body;
  const { id: accountId, balance: newBalance } = data;

  // Crypto deposit detected
  if (
    newBalance.currency.code === "BTC" &&
    newBalance.amount > oldBalance.amount
  ) {
    const depositAmount = newBalance.amount - oldBalance.amount;

    // Record deposit
    await db.deposits.create({
      accountId,
      currency: "BTC",
      amount: depositAmount,
      newBalance: newBalance.amount,
    });

    // Check if user has pending off-ramp
    const pendingOffRamp = await db.offRamps.findPending(accountId);
    if (pendingOffRamp && newBalance.amount >= pendingOffRamp.requiredAmount) {
      // Auto-execute pending off-ramp
      await executeOffRamp(pendingOffRamp.id);
    }
  }
}
```

## Best practices

> [accordion] Process webhooks idempotently
>
> Handle duplicate webhooks gracefully using the envelope `id`:
>
> ```javascript theme={null}
> const { id: webhookId } = req.body;
>
> // Check if already processed
> const existing = await db.webhooks.findUnique({ where: { webhookId } });
> if (existing) {
>   return res.status(200).json({ received: true });
> }
>
> // Process webhook
> await processRampWebhook(req.body);
>
> // Record webhook ID
> await db.webhooks.create({
>   data: { webhookId, processedAt: new Date() }
> });
> ```

> [accordion] Use transaction IDs for deduplication
>
> The transaction lives on `data` and its `id` uniquely identifies the conversion:
>
> ```javascript theme={null}
> const { data: transaction } = req.body;
>
> // Upsert transaction (handles duplicates)
> await db.transactions.upsert({
>   where: { gridTransactionId: transaction.id },
>   update: { status: transaction.status },
>   create: {
>     gridTransactionId: transaction.id,
>     customerId: transaction.customerId,
>     status: transaction.status,
>     // ... other fields
>   },
> });
> ```

> [accordion] Implement retry logic
>
> Grid retries failed webhooks with exponential backoff. Ensure your endpoint can handle retries:
>
> ```javascript theme={null}
> app.post('/webhooks/grid', async (req, res) => {
>   try {
>     await processWebhook(req.body);
>     res.status(200).json({ received: true });
>   } catch (error) {
>     console.error('Webhook processing error:', error);
>
>     // Return 5xx for retryable errors
>     if (error.retryable) {
>       res.status(503).json({ error: 'Temporary failure' });
>     } else {
>       // Return 200 for non-retryable to prevent retries
>       res.status(200).json({ error: error.message });
>     }
>   }
> });
> ```

> [accordion] Monitor webhook health
>
> Track webhook delivery and processing:
>
> ```javascript theme={null}
> // Log webhook metrics
> await metrics.increment('webhooks.received', {
>   type: req.body.type,
>   status: req.body.transaction?.status,
> });
>
> // Track processing time
> const start = Date.now();
> await processWebhook(req.body);
> const duration = Date.now() - start;
>
> await metrics.histogram('webhooks.processing_time', duration, {
>   type: req.body.type,
> });
> ```

## Testing webhooks

Test webhook handling using the test endpoint:

```bash theme={null}
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/webhooks/test' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
```

This sends a test webhook to your configured endpoint:

```json theme={null}
{
  "test": true,
  "timestamp": "2025-10-03T14:32:00Z",
  "id": "Webhook:test001",
  "type": "TEST"
}
```

> [!TIP]
>
> Verify your endpoint receives the test webhook and responds with a 200 status
> code.

## Next steps

* [Sandbox Testing](https://agents-docs.lightspark.com/ramps/platform-tools/sandbox-testing.md) - Test ramp flows end-to-end
* [Platform Configuration](https://agents-docs.lightspark.com/ramps/onboarding/platform-configuration.md) - Configure webhook endpoint
* [Fiat-to-Crypto Conversion](https://agents-docs.lightspark.com/ramps/conversion-flows/fiat-crypto-conversion.md) - Implement conversion flows
* [API Reference](https://agents-docs.lightspark.com/api-reference) - Complete webhook documentation


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.