Webhooks
Configure your app webhook URL to receive asynchronous updates. Verify every webhook signature before you trust the event body.
For retry behavior, delivery guarantees, and callback processing guidance, see Webhook Delivery Guarantees.
Events You Can Subscribe To
payment.createdpayment.completedpayment.failedpayment.refundedpayment.blockedsubscription.createdsubscription.renewedsubscription.cancelledsubscription.reactivatedsubscription.pausedinvoice.paidinvoice.failedsettlement.processed
Webhook Payload (Example)
{
"id": "evt_123",
"type": "payment.completed",
"source": "payment",
"timestamp": "2026-02-09T14:30:00Z",
"data": {
"reference": "TXN-abc123xyz",
"status": "success"
},
"meta": {
"app_id": "app_123",
"version": "v1"
},
"security": {
"signature": "sha256=<hex_signature>",
"algorithm": "HMAC-SHA256"
}
}Node.js / TypeScript signature verification
import crypto from 'crypto';
type WebhookEvent = {
id: string;
type: string;
source: string;
timestamp: string;
data: Record<string, unknown>;
meta: Record<string, unknown>;
security: {
signature: string;
algorithm: 'HMAC-SHA256';
};
};
export function verifyWebhookSignature(event: WebhookEvent, webhookSecret: string): boolean {
if (!event.security?.signature?.startsWith('sha256=')) {
return false;
}
// Must match backend signing payload exactly: JSON with no security field and stable field order.
const payloadWithoutSecurity = {
id: event.id,
type: event.type,
source: event.source,
timestamp: event.timestamp,
data: event.data,
meta: event.meta,
};
const payload = JSON.stringify(payloadWithoutSecurity);
const computed =
'sha256=' +
crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(event.security.signature), Buffer.from(computed));
}Express handler example
import express from 'express';
import { verifyWebhookSignature } from './verify-webhook-signature';
const app = express();
app.use(express.json());
app.post('/webhooks/tinker', (req, res) => {
const event = req.body;
if (!verifyWebhookSignature(event, process.env.TINKER_WEBHOOK_SECRET || '')) {
return res.status(401).json({ success: false, message: 'Invalid webhook signature' });
}
switch (event.type) {
case 'payment.completed':
// Fulfill order
break;
case 'subscription.created':
// Activate customer entitlements
break;
case 'invoice.failed':
// Notify customer to update payment method
break;
default:
// Log and ignore unsupported events
break;
}
return res.status(200).json({ received: true });
});