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

  1. Use the Stripe CLI (`stripe listen --forward-to localhost:PORT/...`) which sends proper signatures.
  2. If behind a proxy, ensure the Stripe-Signature header is forwarded unchanged to the API.
  3. Never send webhook events manually without replicating the signature header.
  4. 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

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)