Webhooks

View as MarkdownOpen in Claude

Webhooks notify your application in real-time when events occur in PaymentKit. Use webhooks to trigger business logic, sync data, and keep your systems up to date.

How webhooks work

When events occur (like a payment succeeding or subscription being created), PaymentKit sends an HTTP POST request to your configured endpoint with details about the event.

Setting up webhooks

  1. Navigate to Developers > Webhooks
  2. Click Add Endpoint
  3. Enter your endpoint URL (must be HTTPS)
  4. Select the events you want to receive
  5. Copy your signing secret for verification

Webhook payload

Webhooks are delivered as JSON with a Stripe-compatible format:

1{
2 "id": "evt_prod_a1b2c3d4e5f6g7h8",
3 "object": "event",
4 "type": "invoice.paid",
5 "created": 1704067200,
6 "livemode": true,
7 "data": {
8 "object": {
9 "id": "in_prod_a1b2c3d4e5f6g7h8",
10 "status": "paid",
11 "currency": "usd",
12 "total_amount_atom": 290000,
13 "due_amount_atom": 0,
14 "paid_amount_atom": 290000
15 },
16 "previous_attributes": {
17 "status": "open",
18 "due_amount_atom": 290000,
19 "paid_amount_atom": 0
20 }
21 }
22}

The data.object field contains the full entity state at the time of the event. The example above is simplified; actual payloads include all entity fields. Amount fields use atomic units (e.g., cents for USD) with the _atom suffix. The previous_attributes field contains the previous state for update events, showing fields that changed.

Webhook headers

Each webhook request includes headers for verification and debugging:

HeaderDescription
Content-Typeapplication/json
X-Webhook-SignatureHMAC signature for verification
X-Webhook-Event-IdUnique event identifier
X-Webhook-Event-TypeEvent type (e.g., invoice.paid)
X-Webhook-Delivery-IdUnique delivery attempt identifier

Verifying signatures

Always verify webhook signatures to ensure requests are from PaymentKit. The signature is an HMAC-SHA256 hash of the raw request body using your endpoint’s signing secret.

1const crypto = require('crypto');
2
3function verifyWebhook(payload, signature, secret) {
4 // Signature format is "sha256=<hex>"
5 const receivedHash = signature.replace('sha256=', '');
6
7 const expected = crypto
8 .createHmac('sha256', secret)
9 .update(payload, 'utf8')
10 .digest('hex');
11
12 // Use timing-safe comparison
13 return crypto.timingSafeEquals(
14 Buffer.from(expected),
15 Buffer.from(receivedHash)
16 );
17}
18
19// In your webhook handler
20app.post('/webhooks', (req, res) => {
21 const signature = req.headers['x-webhook-signature'];
22 const rawBody = req.rawBody; // Ensure you capture raw body
23
24 if (!verifyWebhook(rawBody, signature, WEBHOOK_SECRET)) {
25 return res.status(401).send('Invalid signature');
26 }
27
28 const event = JSON.parse(rawBody);
29 // Process event...
30 res.status(200).send('OK');
31});

Always use the raw request body for signature verification, not a parsed/serialized version. JSON serialization can change formatting and break verification.

Event types

Payment events

EventDescription
payment.succeededPayment completed successfully
payment.failedPayment attempt failed
payment.refundedPayment was refunded

Checkout session events

EventDescription
checkout.session.createdCheckout session created
checkout.session.updatedCheckout session updated
checkout.session.deletedCheckout session deleted
checkout.session.completedCheckout session completed successfully
checkout.session.expiredCheckout session expired

Payment intent events

EventDescription
payment_intent.createdPayment intent created
payment_intent.processingPayment intent is processing
payment_intent.requires_actionPayment intent requires additional action
payment_intent.amount_capturable_updatedCapturable amount updated
payment_intent.succeededPayment intent succeeded
payment_intent.canceledPayment intent was canceled
payment_intent.payment_failedPayment intent payment failed
payment_intent.partially_fundedPayment intent was partially funded

Subscription events

EventDescription
customer.subscription.createdNew subscription created
customer.subscription.updatedSubscription details changed
customer.subscription.pausedSubscription was paused
customer.subscription.resumedSubscription was resumed
customer.subscription.cancelledSubscription was cancelled
customer.subscription.trial_will_endTrial period ending soon

Invoice events

EventDescription
invoice.createdNew invoice created
invoice.finalizedInvoice finalized and ready for payment
invoice.paidInvoice payment succeeded
invoice.payment_succeededInvoice payment succeeded
invoice.payment_failedInvoice payment failed
invoice.payment_action_requiredInvoice payment requires additional action
invoice.overdueInvoice is overdue
invoice.voidedInvoice was voided
invoice.marked_uncollectibleInvoice marked as uncollectible
invoice.refundedInvoice was refunded
invoice.upcomingUpcoming invoice notification

Customer events

EventDescription
customer.createdNew customer created
customer.updatedCustomer details changed
customer.deletedCustomer was deleted

Customer discount events

EventDescription
customer.discount.createdCustomer discount created
customer.discount.updatedCustomer discount updated
customer.discount.deletedCustomer discount deleted

Coupon events

EventDescription
coupon.createdCoupon created
coupon.updatedCoupon updated
coupon.deletedCoupon deleted

Price events

EventDescription
price.createdPrice created
price.updatedPrice updated
price.deletedPrice deleted

Product events

EventDescription
product.createdProduct created
product.updatedProduct updated
product.deletedProduct deleted

Refund events

EventDescription
refund.createdRefund created
refund.updatedRefund updated
refund.failedRefund failed

Retry behavior

If your endpoint returns a server error (5xx status), times out, or is unreachable, PaymentKit automatically retries delivery:

Client errors (4xx status codes) are treated as permanent failures and will not be retried. Ensure your endpoint returns a 2xx status for successful receipt.

RetryDelay
11 minute
25 minutes
330 minutes
42 hours
524 hours

After 5 failed attempts, the delivery is marked as permanently failed. You can manually retry from the dashboard.

Respond to webhooks quickly (within 30 seconds) with a 2xx status. Process event data asynchronously to avoid timeouts.

Best practices

Respond quickly

Return a 2xx response immediately, then process the event asynchronously.

Handle duplicates

Use the event ID to deduplicate. Webhooks may be retried on failure, potentially delivering the same event multiple times.

Verify signatures

Always verify webhook signatures in production to prevent spoofing.

Use HTTPS

Webhook endpoints must use HTTPS. PaymentKit rejects HTTP URLs.

Testing webhooks

You can use tools like ngrok to expose your local development server for webhook testing. This allows you to receive real webhook events during development.

Monitoring deliveries

Track webhook delivery status in the dashboard under Events:

  • delivered - Successfully received (2xx response)
  • pending - Scheduled for initial delivery
  • delivering - Currently being delivered
  • failed - Delivery failed (will retry if attempts remain, or permanent failure if retries exhausted)

View delivery details including attempt count, response status, error messages, and retry timing.