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
Extract signature header
Get the X-Grid-Signature
header from the webhook request. const signature = req . headers [ 'x-grid-signature' ];
Get public key
Retrieve the Grid public key from your dashboard (provided during onboarding).
javascript const publicKey = process.env.GRID_PUBLIC_KEY;
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
Process webhooks idempotently
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 () }
});
Use transaction IDs for deduplication
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