Webhooks
Receive real-time notifications for payment and order events
Headers
Every webhook request includes:
| Header | Description |
|---|---|
X-Webhook-Event | Event type (e.g. checkout.completed) |
X-Webhook-Id | Unique delivery ID for idempotency |
Content-Type | application/json |
Delivery
- Attempts: 5 retries on failure
- Backoff: exponential with 5-second base (5s, 10s, 20s, 40s, 80s)
- Timeout: 10 seconds per attempt
- Method: POST
Your endpoint must return a 2xx status code to acknowledge receipt. Any other status triggers a retry.
Events
checkout.completed
Fired when a checkout session payment is confirmed on-chain.
{
"event": "checkout.completed",
"sessionId": "sess_abc123",
"transactionId": "tx_def456",
"txHash": "0x8f3d...a2c1",
"amount": "49.00",
"skillId": "skill_xyz",
"type": "subscription",
"metadata": { "planId": "premium-30d" }
}payment.confirmed
Fired when an x402 API payment is verified.
{
"event": "payment.confirmed",
"transactionId": "tx_def456",
"txHash": "0x8f3d...a2c1",
"amount": "0.05",
"skillId": "skill_xyz",
"walletAddress": "0x742d...5f0b"
}subscription.created
Fired when a new subscription is created.
{
"event": "subscription.created",
"transactionId": "tx_def456",
"txHash": "0x8f3d...a2c1",
"skillId": "skill_xyz",
"amount": "49.00",
"durationDays": 30,
"expiresAt": "2026-03-23T00:00:00Z"
}subscription.expired
Fired when a subscription reaches its expiration date.
{
"event": "subscription.expired",
"subscriptionId": "sub_abc123",
"skillId": "skill_xyz",
"expiredAt": "2026-03-23T00:00:00Z"
}refund.completed
Fired when a refund is processed and USDC is returned to the buyer.
{
"event": "refund.completed",
"transactionId": "tx_abc123",
"refundTxHash": "0x1a2b...3c4d",
"amount": "49.00"
}refund.requested
Fired when a buyer requests a refund (pending merchant approval).
{
"event": "refund.requested",
"transactionId": "tx_abc123",
"reason": "Service not as described"
}order.created
Fired when a product order is created.
{
"event": "order.created",
"orderId": "ord_abc123",
"transactionId": "tx_def456",
"txHash": "0x8f3d...a2c1",
"skillId": "skill_xyz",
"amount": "99.99",
"quantity": 1,
"requiresShipping": true
}Idempotency
Use the X-Webhook-Id header to deduplicate deliveries. Store processed webhook IDs and skip any you've already handled:
app.post('/webhooks/402md', (req, res) => {
const webhookId = req.headers['x-webhook-id']
if (await isProcessed(webhookId)) {
return res.sendStatus(200)
}
const event = req.body
switch (event.event) {
case 'checkout.completed':
await handleCheckout(event)
break
case 'refund.completed':
await handleRefund(event)
break
}
await markProcessed(webhookId)
res.sendStatus(200)
})Dashboard
View webhook delivery logs and retry failed deliveries from the 402.md dashboard. Each delivery shows the request payload, response status, and timing.