Webhooks
Webhooks
RefundKit sends webhook events to your server when refund statuses change. This lets you react to refund events in real time instead of polling the API.
Supported Events
| Event | Trigger |
|-------|---------|
| refund.created | A new refund has been created |
| refund.processing | The refund has been sent to the payment processor |
| refund.completed | The refund was successfully processed |
| refund.failed | The refund failed at the processor level |
| refund.cancelled | The refund was cancelled |
Webhook Payload
All webhook events share the same envelope format:
{
"id": "evt_abc123def456",
"type": "refund.completed",
"createdAt": "2026-02-22T10:32:15.000Z",
"data": {
"id": "ref_abc123def456",
"organizationId": "org_xyz789",
"externalRefundId": "re_1N4HcTKz9cXRvFYs",
"transactionId": "ch_1N4HbSKz9cXRvFYr",
"amount": 2500,
"currency": "usd",
"reason": "product_defective",
"status": "completed",
"processor": "stripe",
"metadata": {
"orderId": "order_12345"
},
"initiatedBy": "api",
"createdAt": "2026-02-22T10:30:00.000Z",
"updatedAt": "2026-02-22T10:32:15.000Z"
}
}
Payload Fields
| Field | Type | Description |
|-------|------|-------------|
| id | string | Unique event ID for deduplication |
| type | string | Event type (e.g., refund.completed) |
| createdAt | string | ISO 8601 timestamp of when the event was generated |
| data | Refund | The full refund object at the time of the event |
Configuring Webhooks
Via the Dashboard
- Navigate to Settings > Webhooks in the RefundKit Dashboard.
- Click Add Endpoint.
- Enter your endpoint URL (must be HTTPS in production).
- Select the events you want to receive.
- Copy the signing secret for signature verification.
Signature Verification
Every webhook request includes a RefundKit-Signature header containing an HMAC-SHA256 signature. Always verify this signature to ensure the request is authentic.
Header Format
RefundKit-Signature: t=1708617135,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
The header contains a timestamp (t) and signature (v1), separated by a comma.
Verification in Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string,
toleranceSeconds = 300,
): boolean {
const parts = Object.fromEntries(
signature.split(',').map((part) => {
const [key, value] = part.split('=');
return [key, value];
}),
);
const timestamp = parseInt(parts.t, 10);
const now = Math.floor(Date.now() / 1000);
// Reject if timestamp is too old (replay protection)
if (Math.abs(now - timestamp) > toleranceSeconds) {
return false;
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expected = createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Timing-safe comparison
return timingSafeEqual(
Buffer.from(parts.v1),
Buffer.from(expected),
);
}
Express Example
import express from 'express';
const app = express();
app.post(
'/webhooks/refundkit',
express.raw({ type: 'application/json' }),
(req, res) => {
const payload = req.body.toString();
const signature = req.headers['refundkit-signature'] as string;
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
switch (event.type) {
case 'refund.completed':
console.log(`Refund ${event.data.id} completed for $${event.data.amount / 100}`);
// Update your database, notify customer, etc.
break;
case 'refund.failed':
console.log(`Refund ${event.data.id} failed: investigate`);
break;
}
res.status(200).send('OK');
},
);
Retry Policy
If your endpoint does not return a 2xx status code, RefundKit retries the webhook delivery with exponential backoff:
| Attempt | Delay | |---------|-------| | 1st retry | 1 minute | | 2nd retry | 5 minutes | | 3rd retry | 30 minutes | | 4th retry | 2 hours | | 5th retry | 24 hours |
After 5 failed retries, the event is marked as failed. You can view failed deliveries and manually retry them from the dashboard.
Best Practices
- Always verify signatures to prevent forged webhook requests.
- Return 200 quickly and process events asynchronously. If your handler takes too long, the request may time out and trigger a retry.
- Use the event
idfor deduplication. Retries send the same event ID, so check whether you have already processed it. - Use HTTPS endpoints in production for security.
- Handle events idempotently. Your handler should produce the same result whether called once or multiple times with the same event.