Webhook Setup & Configuration
Receive real-time payment notifications via HTTP POST callbacks
Webhooks allow GriffNode to push payment events to your server the moment they occur, no polling required. When a payment completes, expires, or detects a partial payment, we send a signed HTTP POST to your configured endpoint.
Creating a Webhook
- Go to Dashboard → Webhooks → Add Endpoint
- Enter your endpoint URL (must be publicly accessible HTTPS)
- Select the events you want to receive
- Copy your webhook secret, it is only shown once
The secret is shown only at creation time. If you lose it, you must regenerate it from the dashboard, which will invalidate the old secret.
Webhook Request Headers
| Header | Description |
|---|---|
X-GriffNode-Signature |
HMAC-SHA256 signature of the raw request body, formatted as sha256=<hex> |
X-GriffNode-Event |
Event type, e.g. payment.completed |
X-Webhook-ID |
ID of your webhook endpoint, e.g. WHK-XXXXXXXX. The same on every delivery; to detect duplicates use the payload's id. |
X-GriffNode-Attempt |
Delivery attempt number: 1 for the first send, higher on retries. |
Content-Type |
application/json |
Webhook Payload
{
"id": "evt_6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
"event": "payment.completed",
"timestamp": "2026-03-08T10:30:00Z",
"transaction_id": "MTX-A1B2C3D4",
"status": "completed",
"currency_crypto": "BTC",
"amount_fiat": "25.00",
"amount_crypto": "0.00025431"
}
Responding to Webhooks
Your endpoint must return HTTP 2xx within 10 seconds. Any other status, a timeout or a connection error counts as a failed delivery, and GriffNode retries automatically with growing gaps: after about 10 seconds, 1 minute, 5 minutes, 30 minutes, then 1, 2, 4 and 8 hours, for up to 24 hours after the event. After that the delivery is marked failed.
- Handle duplicates. A retry can arrive even if you processed the first attempt (for example when your response timed out). Every event has a stable
idin the payload that is identical on every retry: store it and skip events you've already handled. - Don't rely on order. A retried earlier event (say
payment.pending) can arrive after a later one (payment.completed). Use the payload'stimestampor check the current state withGET /v1/transactions/{transaction_id}. - Reconcile money. For anything that affects money, confirm with
GET /v1/transactions/{transaction_id}before acting.
Never process a webhook without first verifying the X-GriffNode-Signature header. See Signature Verification.
Example Receiver (Node.js)
import crypto from 'crypto';
import express from 'express';
const app = express();
// IMPORTANT: use raw body for signature verification
app.post('/webhooks/griffnode', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['x-griffnode-signature'] || '';
const secret = process.env.WEBHOOK_SECRET;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (sig !== expected) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body);
if (await alreadyProcessed(event.id)) return res.sendStatus(200); // retries reuse the same id
switch (event.event) {
case 'payment.completed':
// Fulfil the order
console.log('Payment completed for', event.transaction_id);
break;
case 'payment.partial':
console.log('Partial payment for', event.transaction_id);
break;
case 'payment.expired':
console.log('Transaction expired:', event.transaction_id);
break;
}
res.status(200).send('OK');
});