Skip to main content
GPU Outlet’s webhook endpoint lives at:
POST https://gpuoutlet.ai/api/v1/webhooks/stripe
(The server route is /v1/webhooks/stripe; the /api/ prefix is added by the production reverse proxy.) It accepts only Stripe-signed payloads — every request is verified against STRIPE_WEBHOOK_SECRET before any side effects fire. Unsigned or unverifiable requests get a 400 invalid_signature and are not processed. If you’re standing up a fresh Stripe environment, configure this endpoint in Stripe Dashboard → Developers → Webhooks and subscribe to the events below.

Subscribed events

Top-ups (one-off & auto)

EventWhat it does
checkout.session.completedOne-off Stripe Checkout finished. Credits the wallet by amount_total cents. Idempotent on the payment-intent id.
payment_intent.succeededSaved-card top-up (off-session) or Checkout PI fallback. Same credit logic — same idempotency key, so subscribing to both never double-credits.
setup_intent.succeededUser added a card. We persist brand/last4/exp + set it as the customer’s default payment method.

Invoices (added v0.5)

EventWhat it does
invoice.paidFlips our local invoice to paid, records paidAt + receiptUrl. For topup_failed invoices, also credits the wallet by amountCents (idempotent on invoice_paid_<id>).
invoice.payment_succeededStripe fires this alongside invoice.paid in practice; same handler. Idempotent.
invoice.voidedReflects Stripe-side void onto our copy. Status → void, voidedAt set.
invoice.marked_uncollectibleStripe auto-writes-off invoices that go 30+ days unpaid. Status → uncollectible.

Idempotency

Two layers, defence-in-depth:
  1. Event-id dedupe: every processed evt_… id is written to a stripe_events table. Redelivery of the same event short-circuits with a {received: true, duplicate: true} response.
  2. Ledger key dedupe: every wallet movement triggered by a webhook has a unique idempotencyKey (deposit_<pi_id>, invoice_paid_<inv_id>, etc). A retried event that somehow slipped past layer 1 still can’t double-post.

Failure modes

FailureWhat happensHow we recover
Bad signature400 invalid_signature returned, no DB writes.Check STRIPE_WEBHOOK_SECRET matches the endpoint secret in Stripe.
Unknown event typeSilently ignored (returns {received: true}).We add a handler when we want it to do something.
Database error during processingThrows → Stripe retries with backoff.Retries are safe due to idempotency.
Stripe-side data we can’t map (e.g. invoice with no ourInvoiceId metadata)Logged + skipped.The Stripe-side invoice still gets paid; our copy stays stale. Manual reconciliation only.

Testing locally

Stripe CLI can forward production webhooks to localhost during dev:
stripe listen --forward-to localhost:3000/v1/billing/stripe/webhook
The CLI prints a temporary webhook secret — set it as STRIPE_WEBHOOK_SECRET in your local env. Then trigger events with stripe trigger:
stripe trigger invoice.paid
stripe trigger payment_intent.succeeded

Replay

In Stripe Dashboard → Webhooks → your endpoint → Events, every delivery attempt is logged with the full payload. Click any event and hit Resend to redeliver — our idempotency makes this safe.