Guide
eSigKit’s webhook surface today is inbound only — we receive Stripe events
to drive subscription state. Outbound webhooks (eSigKit emitting events
to your endpoints, e.g., signature.rendered, audit.event) are roadmap and
not currently implemented. This page documents the inbound Stripe path so
self-hosted Stripe accounts integrating with eSigKit know what to expect.
Stripe → eSigKit
When Stripe events fire on your subscription, Stripe POSTs them to
https://api.esigkit.com/v1/webhooks/stripe. The handler verifies the
signature, dedups against a 7-day idempotency window, and patches the org’s
subscription state in DynamoDB.
This endpoint is registered for you automatically when you subscribe via Checkout — you don’t configure anything Stripe-side.
Request shape
POST /v1/webhooks/stripe HTTP/1.1
Host: api.esigkit.com
Content-Type: application/json
Stripe-Signature: t=1746745260,v1=abc123def...
{
"id": "evt_test_REPLACEME",
"type": "invoice.paid",
"data": { "object": { ... } }
}
Signature verification
HMAC-SHA256 via the stripe-signature header. eSigKit verifies via
stripe.webhooks.constructEvent with a 5-minute timestamp tolerance (Stripe
SDK default). The shared secret lives in SSM at
/esigkit/{stage}/stripe/webhook-secret.
// Self-host equivalent — for reference only; you don't run this on your side
import Stripe from 'stripe';
const event = Stripe.webhooks.constructEvent(rawBody, signature, secret);
Idempotency
Every event is dedup-keyed on event.id in the Idempotency table with a
7-day TTL (covers Stripe’s 3-day retry window plus margin). Re-receiving an
event ID returns:
{
"received": true,
"deduplicated": true
}
Retry semantics
The handler always returns 200 once the signature verifies. Stripe retries on 5xx, so we deliberately swallow handler-side failures into 200 responses to stop the retry loop — failures go to CloudWatch with the correlation ID for ops follow-up.
{ "received": true, "kind": "patch" }
Event-type catalog
| Event | What eSigKit does |
|---|---|
checkout.session.completed | Mark org as active, set currentPeriodEnd, exit trial. |
customer.subscription.created | Set subscriptionStatus, plan, price version, currentPeriodEnd. |
customer.subscription.updated | Re-sync plan + status. Trigger recomputeOverLimit() if seat count changes. |
customer.subscription.deleted | Mark org cancelled. Set gracePeriodEndsAt = now + 14d. |
invoice.paid | Mark org active if it was past-due, clear gracePeriodEndsAt. |
invoice.payment_failed | Set subscriptionStatus = past_due, start 14-day grace. After grace, withDunningCheck starts returning 402 on writes. |
Verifying signatures yourself (for testing)
If you’re forwarding eSigKit’s Stripe traffic to a local listener via the Stripe CLI:
stripe listen --forward-to localhost:3000/webhook
The CLI signs forwarded events with a stable test secret you can read from
stripe listen output. Use that secret to verify in your local handler.
This isn’t usually necessary — eSigKit’s handler IS the webhook receiver and runs on AWS. But if you’re building a service that mirrors eSigKit-style webhook handling, the pattern is the same.