Stripe Webhooks
Receives billing lifecycle events from Stripe. This endpoint is public (no authentication required) -- security is enforced through Stripe signature verification.
Receive a Stripe event
POST /v1/webhooks/stripe
Processes incoming Stripe webhook events for billing lifecycle management.
Headers
| Header | Required | Description |
|---|---|---|
Stripe-Signature | Yes | Stripe webhook signature (t=<timestamp>,v1=<signature>) |
Request body
The raw Stripe event JSON payload (max 64 KiB).
Signature verification
FiscalAPI verifies every Stripe webhook delivery:
- Extract the
Stripe-Signatureheader - Parse the timestamp (
t=) and signature (v1=) - Verify the timestamp is within ±5 minutes of server time
- Compute
HMAC-SHA256(webhook_secret, "{timestamp}.{payload}") - Compare against the provided signature using constant-time comparison
Requests that fail signature verification are rejected with 400 Bad Request.
Processed events
| Stripe Event | Action |
|---|---|
invoice.payment_succeeded | Account restored if previously suspended |
invoice.payment_failed | Account suspended, notification email sent |
customer.subscription.deleted | Account suspended, notification email sent |
customer.subscription.updated | Update logged |
Idempotency
Each event is processed exactly once. Duplicate deliveries (same Stripe event ID) return 200 OK without reprocessing.
Response
| Status | Description |
|---|---|
200 OK | Event processed (or duplicate) |
400 Bad Request | Invalid signature or payload |
Example
# Stripe sends this automatically -- you do not call this endpoint directly.
# Configure your Stripe webhook to point to:
# https://api.fiscalapi.com/v1/webhooks/stripe
#
# Required Stripe events:
# - invoice.payment_succeeded
# - invoice.payment_failed
# - customer.subscription.deleted
# - customer.subscription.updated
Verifying signatures (reference)
If you want to understand how Stripe signature verification works:
import hmac
import hashlib
import time
def verify_stripe_signature(payload, header, secret, tolerance=300):
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp = parts["t"]
signature = parts["v1"]
# Check timestamp tolerance (±5 minutes)
if abs(time.time() - int(timestamp)) > tolerance:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.{payload}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
const crypto = require("crypto");
function verifyStripeSignature(payload, header, secret, tolerance = 300) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=", 1))
);
const timestamp = parts["t"];
const signature = parts["v1"];
// Check timestamp tolerance (±5 minutes)
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > tolerance) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${payload}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}