ApplicationsCheckout

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

plaintext
POST /api/webhooks/{provider}/{org}
GET  /api/webhooks/{provider}/{org}  (provider verification)
SegmentValues
providerstripe, easypay, ifthenpay
orgOrganization ID (tenant)

Example URLs

plaintext
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_abc123

Flow

Drag to pan · Scroll to zoom

Per-tenant verification

Each store connects its own provider in the dashboard (Integrations page). The verification flow:

  1. Load the store's integration_config for the provider
  2. Decrypt its credentials
  3. 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.

ts
// @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

  1. In Stripe Dashboard → Webhooks → Add endpoint
  2. URL: https://checkout.example.com/api/webhooks/stripe/{orgId}
  3. Events: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired, charge.refunded
  4. Copy signing secret → dashboard Integrations → Stripe → webhook secret

Or run ./scripts/stripe-webhook-setup.sh to configure via CLI.

Easypay

  1. Configure webhook URL in Easypay merchant panel
  2. URL: https://checkout.example.com/api/webhooks/easypay/{orgId}
  3. Easypay sends payment status notifications

Ifthenpay

  1. Configure callback URL in Ifthenpay backoffice
  2. URL: https://checkout.example.com/api/webhooks/ifthenpay/{orgId}
  3. Anti-phishing key used for verification

Webhook as safety net

Webhooks complement the sync-on-return flow:

ScenarioPrimaryFallback
Stripe card paymentcheckout.confirm() on returnWebhook 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:

plaintext
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.