Webhooks
Payment webhook routing, verification, and forwarding in the checkout app.
Payment providers send webhooks to notify about async payment events (Multibanco payments, 3DS completions, refunds). The checkout app receives these webhooks and forwards them to the Commerce API for order reconciliation.
Webhook routes
POST /api/webhooks/{provider}/{org}
GET /api/webhooks/{provider}/{org} (provider verification)| Segment | Values |
|---|---|
provider | stripe, easypay, ifthenpay |
org | Organization ID (tenant) |
Example URLs
POST https://checkout.example.com/api/webhooks/stripe/org_abc123
POST https://checkout.example.com/api/webhooks/easypay/org_abc123
POST https://checkout.example.com/api/webhooks/ifthenpay/org_abc123Flow
Drag to pan · Scroll to zoom
Per-tenant verification
Each store connects its own provider in the dashboard (Integrations page). The verification flow:
- Load the store's
integration_configfor the provider - Decrypt its credentials
- Verify the call with
provider.verifyWebhook()
What that means depends on the provider. Stripe checks the signature against the store's webhookSecret. Ifthenpay checks the anti-phishing key. Easypay notifications are unsigned, so the body is only a trigger: the payment is read back from Easypay's API, and that response is the source of truth.
There is no environment fallback.
// @prood/commerce
export async function verifyPaymentWebhook(tenantId, providerId, payload, signature) {
const { provider } = await getTenantPaymentProvider(tenantId, providerId, { includeDisabled: true })
return provider.verifyWebhook(payload, signature)
}Provider-specific setup
Stripe
- In Stripe Dashboard → Webhooks → Add endpoint
- URL:
https://checkout.example.com/api/webhooks/stripe/{orgId} - Events:
checkout.session.completed,checkout.session.async_payment_succeeded,checkout.session.async_payment_failed,checkout.session.expired,charge.refunded - Copy signing secret → dashboard Integrations → Stripe → webhook secret
Or run ./scripts/stripe-webhook-setup.sh to configure via CLI.
Easypay
- Configure webhook URL in Easypay merchant panel
- URL:
https://checkout.example.com/api/webhooks/easypay/{orgId} - Easypay sends payment status notifications
Ifthenpay
- Configure callback URL in Ifthenpay backoffice
- URL:
https://checkout.example.com/api/webhooks/ifthenpay/{orgId} - Anti-phishing key used for verification
Webhook as safety net
Webhooks complement the sync-on-return flow:
| Scenario | Primary | Fallback |
|---|---|---|
| Stripe card payment | checkout.confirm() on return | Webhook checkout.session.completed |
| Customer closes browser during 3DS | — | Webhook checkout.session.completed updates order |
| Stripe async payment (bank transfer) | — | Webhook checkout.session.async_payment_succeeded |
| Multibanco async payment | — | Webhook when customer pays at ATM |
| MB WAY push notification | — | Webhook on phone confirmation |
The checkout state machine's handleWebhookUpdate() can transition from any non-terminal state based on webhook data.
API webhook endpoint
The Commerce API also exposes a direct webhook endpoint:
POST /v1/webhooks/payments/{provider}?org={orgId}
Header: x-checkout-secret: {CHECKOUT_API_SECRET}This is the target of forwardPaymentWebhook(). It can also be called directly if webhooks are routed to the API domain instead of the checkout domain.