moeru-ai/airi · error · ApiError
MISSING_SIGNATURE
MISSING_SIGNATURE
Error message
No signature
What it means
Stripe signs webhook deliveries with a signature header (Stripe-Signature). The webhook operation requires this header to verify authenticity; a request without it is rejected with 400 MISSING_SIGNATURE before any event parsing.
Solutions
- Use the Stripe CLI (`stripe listen --forward-to localhost:PORT/...`) which sends proper signatures.
- If behind a proxy, ensure the Stripe-Signature header is forwarded unchanged to the API.
- Never send webhook events manually without replicating the signature header.
- Check that the client/request library is not stripping custom headers.
Example fix
// before
curl -X POST http://localhost:3000/api/webhooks/stripe -d '{"type":"x"}'
// after
stripe listen --forward-to localhost:3000/api/webhooks/stripe Defensive patterns
Strategy: validation
Validate before calling
const signature = req.headers.get('stripe-signature')
if (!signature)
throw new Error('request missing stripe-signature header; not a genuine Stripe delivery') Try / catch
try {
const res = await fetch(webhookUrl, { method: 'POST', headers: { 'Stripe-Signature': signature, 'Content-Type': 'application/json' }, body: rawBody })
} catch (e) {
if (e.code === 'MISSING_SIGNATURE') {
// ensure your HTTP client forwards the signature header
}
} Prevention
- Always forward the Stripe-Signature header verbatim through proxies (check Caddy/nginx config).
- Test webhooks with `stripe listen --forward-to` instead of manual curl.
- Never re-serialize webhook bodies before verification.
When it happens
Trigger: POSTing to the webhook endpoint without the Stripe-Signature header: manual curl tests, proxies/load balancers stripping the header, or a misconfigured reverse proxy that does not forward raw headers.
Common situations: Testing the endpoint by hand with a raw JSON body; Caddy/nginx config dropping the signature header; hitting the endpoint with a tool that doesn't replicate Stripe's headers.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17).
Data as JSON: /api/errors/d782edf9a3c2b3d5.
Report an issue: GitHub.
Appendix: source
Thrown at server/apps/api/src/routes/stripe/operations/webhook.ts:97
/**
* Verifies a Stripe webhook, maps a Checkout Session to a claim receipt,
* then calls Payment CORE. Unknown events are ignored.
*/
export function createWebhookOperation(
stripe: Stripe | null,
webhookSecret: string | null,
payment: PaymentService,
db: Database,
metrics: RevenueMetrics | null,
productEventService: ProductEventService | null,
) {
return async (signature: string | null, body: string): Promise<{ received: true }> => {
if (!stripe || !webhookSecret)
throw createServiceUnavailableError('Stripe is not configured', 'STRIPE_NOT_CONFIGURED')
if (!signature)
throw createBadRequestError('No signature', 'MISSING_SIGNATURE')
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
}
catch (err: unknown) {
throw createBadRequestError(`Webhook Error: ${errorMessageFromUnknown(err)}`, 'WEBHOOK_ERROR')
}
logger.withFields({ type: event.type, id: event.id }).log('Webhook event received')
metrics?.stripeEvents.add(1, { event_type: event.type })
switch (event.type) {
case 'checkout.session.completed':
case 'checkout.session.async_payment_succeeded': {
const session = parse(checkoutSessionSchema, event.data.object)
if (session.mode !== 'payment') {
logger.withFields({ sessionId: session.id, mode: session.mode }).log('Ignoring non-payment checkout session')View on GitHub (pinned to 438a067dde)