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) or _ for env-fallback |
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/_Flow
Drag to pan · Scroll to zoom
Per-tenant verification
Each tenant can configure their own webhook secret in the dashboard (Integrations page). The verification flow:
- Load tenant's
integration_configfor the provider - Decrypt stored
webhookSecret - Verify signature against tenant's secret
- Fall back to env var (
STRIPE_WEBHOOK_SECRET, etc.) if org is_or secret not configured
// @prood/commerce
export async function verifyPaymentWebhook(
payload: string,
signature: string,
providerId: string,
orgId: string,
): Promise<PaymentWebhookResult> {
const config = await loadIntegrationConfig(orgId, providerId)
const provider = getPaymentProvider(providerId, config)
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 (11 total):
checkout.session.completed,checkout.session.expired,checkout.session.async_payment_succeeded,checkout.session.async_payment_failedpayment_intent.succeeded,payment_intent.payment_failed,payment_intent.canceled,payment_intent.processing,payment_intent.amount_capturable_updatedcharge.refunded,refund.created
- 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.