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.created
  • payment.completed
  • payment.failed
  • payment.refunded
  • payment.blocked
  • subscription.created
  • subscription.renewed
  • subscription.cancelled
  • subscription.reactivated
  • subscription.paused
  • invoice.paid
  • invoice.failed
  • settlement.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 });
});