Skip to main content
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

  • OUTGOING_PAYMENT
  • ACCOUNT_STATUS
  • KYC_STATUS
Sent when a conversion (on-ramp or off-ramp) completes, fails, or changes status.
{
  "transaction": {
    "id": "Transaction:019542f5-b3e7-1d02-0000-000000000025",
    "status": "COMPLETED",
    "type": "OUTGOING",
    "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"
  },
  "timestamp": "2025-10-03T15:03:00Z",
  "webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000030",
  "type": "OUTGOING_PAYMENT"
}
Use this webhook to update your UI, credit customer accounts, and trigger post-conversion workflows.

Webhook configuration

Configure your webhook endpoint in the Grid dashboard or via API:
curl -X PATCH 'https://api.lightspark.com/grid/2025-10-13/config' \
  -u "$GRID_CLIENT_ID:$GRID_API_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "webhookEndpoint": "https://api.yourapp.com/webhooks/grid"
  }'
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.
const signature = req.headers['x-grid-signature'];
2

Get public key

Retrieve the Grid public key from your dashboard (provided during onboarding). javascript const publicKey = process.env.GRID_PUBLIC_KEY;
3

Verify signature

Use the public key to verify the signature against the raw request body.
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' });
}
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:
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, transaction } = req.body;

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

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

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

Off-ramp completion

Handle successful crypto-to-fiat conversions:
if (type === "OUTGOING_PAYMENT" && transaction.status === "COMPLETED") {
  // Off-ramp completed (BTC → USD)
  if (
    transaction.sentAmount.currency.code === "BTC" &&
    transaction.receivedAmount.currency.code === "USD"
  ) {
    // Update user's balance and transaction history
    await db.transactions.create({
      userId: transaction.customerId,
      type: "OFF_RAMP",
      amountBtc: transaction.sentAmount.amount,
      amountUsd: transaction.receivedAmount.amount,
      rate: transaction.exchangeRate,
      status: "COMPLETED",
      bankAccountId: transaction.destination.accountId,
      completedAt: new Date(transaction.settledAt),
    });

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

Balance updates

Track crypto deposits for off-ramp liquidity:
if (type === "ACCOUNT_STATUS") {
  const { accountId, newBalance, oldBalance } = req.body;

  // 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

Handle duplicate webhooks gracefully using webhook IDs:
const { 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() }
});
Transaction IDs are unique identifiers for conversions:
const { 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
  },
});
Grid retries failed webhooks with exponential backoff. Ensure your endpoint can handle retries:
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 });
    }
  }
});
Track webhook delivery and processing:
// 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:
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/webhooks/test' \
  -u "$GRID_CLIENT_ID:$GRID_API_SECRET"
This sends a test webhook to your configured endpoint:
{
  "test": true,
  "timestamp": "2025-10-03T14:32:00Z",
  "webhookId": "Webhook:test001",
  "type": "TEST"
}
Verify your endpoint receives the test webhook and responds with a 200 status code.

Next steps

I