Integration

Webhooks

AgentPay receives Stripe webhooks for card, authorization, and payment-intent events. The handler lives at /v1/webhooks/stripe. If you need to forward events to your own system, you can subscribe to an outbound webhook — coming in v0.1. For now, here's exactly what we consume and how.

Events we handle

EventWhat we do
setup_intent.succeededSafety-net: records the saved PM if the SetupIntent flow was interrupted before /api/setup/complete ran.
payment_intent.amount_capturable_updatedMarks the card_hold row as requires_capture in our DB.
payment_intent.succeededMarks the card_hold as captured.
payment_intent.canceledMarks the card_hold as canceled.
issuing_authorization.createdA card was used at a merchant. We capture the PaymentIntent hold and mark the card USED.
issuing_card.updatedSyncs card status (active / canceled) from Stripe to our DB.
account.updatedNo-op. We read live state when we need it.

Signature verification

Every webhook is verified via stripe.webhooks.constructEvent using the STRIPE_WEBHOOK_SECRET env var. Events with invalid signatures return 400 invalid_request. In development, if no secret is set, we parse the body directly — do not do this in production.

Idempotency

Every event is stored in webhook_events keyed by the Stripe event id with a unique constraint. Duplicate deliveries short-circuit with {"ok":true,"duplicate":true}. This matters because Stripe retries a webhook up to 3 days if you return non-2xx.

Testing webhooks locally

Use the Stripe CLI to forward events to your dev server:

# Forwards platform-level events to your dev server
stripe listen --forward-to http://localhost:3100/v1/webhooks/stripe

# In another tab, trigger a test event
stripe trigger payment_intent.succeeded

Retry behavior

If our handler returns non-2xx, Stripe retries with exponential backoff for up to 3 days. Our handler wraps dispatch in try/catch and returns 500 on any exception — Stripe will retry until success. The idempotency guard prevents double-processing.