POST /v1/webhooks/stripe
| Call | POST /v1/webhooks/stripe |
| Price | free |
| Scope | none (public endpoint) |
| Operation id | receiveStripeWebhook |
What it does
Section titled “What it does”Stripe calls this endpoint; agents never do. Operator setup: in the Stripe dashboard (test mode for staging, live mode for production) add a webhook endpoint with this URL, subscribe it to checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed and checkout.session.expired, and store its signing secret (whsec_...) in Doppler as STRIPE_WEBHOOK_SECRET. Verdict verifies Stripe-Signature over the raw body before parsing anything (401 unauthorized on failure, nothing stored), reads the organization from the event’s metadata.org_id and checks it against the pending pack named by metadata.pack_id (400 invalid_request when no organization resolves, nothing stored), records the event id, and credits the pack once when payment is confirmed. Idempotent on the Stripe event id: a replay returns 200 with duplicate: true and credits nothing; the same id with a different payload is refused with 400. Other event types are recorded and ignored. Free.
Example request
Section titled “Example request”Copy it as curl. Set VERDICT_API_KEY first; uuidgen makes a fresh Idempotency-Key.
curl -X POST 'https://api.askverdict.io/v1/webhooks/stripe' \ -H 'Stripe-Signature: t=1794060000,v1=5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e' \ -H 'Content-Type: application/json' \ -d '{ "id": "evt_1QExampleCheckoutCompleted", "object": "event", "type": "checkout.session.completed", "livemode": false, "data": { "object": { "id": "cs_test_example", "object": "checkout.session", "mode": "payment", "payment_status": "paid", "amount_total": 5000, "currency": "usd", "client_reference_id": "org_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "metadata": { "org_id": "org_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "pack_id": "pck_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "kind": "pack" } } }}'Example response
Section titled “Example response”200: The event was verified and recorded.
{ "received": true, "duplicate": false}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
stripe-signature |
header | string | yes | Stripe’s signature over the raw body (t=<unix seconds>,v1=<hex HMAC-SHA256>), computed with the endpoint’s signing secret. A missing or wrong signature, or a timestamp more than 5 minutes off, is refused with 401 unauthorized and nothing is stored. |
Request body
Section titled “Request body”Schema StripeWebhookBody. The Stripe event, unchanged.
Type: object.
Response fields (200)
Section titled “Response fields (200)”Schema StripeWebhookReceipt.
| Field | Type | Required | Description |
|---|---|---|---|
received |
enum: true | yes | The event was verified and recorded. |
duplicate |
boolean | yes | True when this Stripe event id was already recorded; nothing changed. |
Errors
Section titled “Errors”Errors are RFC 9457 problem details and are never billed. Branch on type. See Errors.
| Status | Meaning |
|---|---|
400 |
Invalid request: malformed JSON, an unknown field, a failed constraint or a size limit (invalid_request). |
401 |
Missing, malformed, revoked or expired API key, or a bad webhook signature (unauthorized). |
404 |
The resource does not exist in this organization (not_found). |
429 |
Rate limited (rate_limited). Wait Retry-After seconds. |
500 |
Unexpected server error (internal). Not billed; retry with the same Idempotency-Key. |
Try it
Section titled “Try it”Open the interactive reference to send this request from the browser. The full document is at /openapi.json.