{"record":{"id":"7030df7bb77979a7","repo":"calcom/cal.diy","slug":"missing-or-invalid-signature-format","errorCode":null,"errorMessage":"Missing or invalid signature format","messagePattern":"Missing or invalid signature format","errorType":"http","errorClass":"HttpCode","httpStatus":401,"severity":"error","filePath":"packages/app-store/btcpayserver/api/webhook.ts","lineNumber":52,"sourceCode":"  type: z.string(),\r\n  timestamp: z.number(),\r\n  storeId: z.string(),\r\n  invoiceId: z.string(),\r\n  metadata: z.object({}).optional(),\r\n  manuallyMarked: z.boolean().optional(),\r\n  overPaid: z.boolean(),\r\n});\r\nconst SUPPORTED_INVOICE_EVENTS = [\"InvoiceSettled\", \"InvoiceProcessing\"];\r\n\r\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\r\n  try {\r\n    if (req.method !== \"POST\") throw new HttpCode({ statusCode: 405, message: \"Method Not Allowed\" });\r\n    const rawBody = await getRawBody(req);\r\n    const bodyAsString = rawBody.toString();\r\n\r\n    const signature = req.headers[\"btcpay-sig\"] || req.headers[\"BTCPay-Sig\"];\r\n    if (!signature || typeof signature !== \"string\" || !signature.startsWith(\"sha256=\"))\r\n      throw new HttpCode({ statusCode: 401, message: \"Missing or invalid signature format\" });\r\n\r\n    const webhookData = btcpayWebhookSchema.safeParse(JSON.parse(bodyAsString));\r\n    if (!webhookData.success) return res.status(400).json({ message: \"Invalid webhook payload\" });\r\n\r\n    const data = webhookData.data;\r\n    if (!SUPPORTED_INVOICE_EVENTS.includes(data.type))\r\n      return res.status(200).send({ message: \"Webhook received but ignored\" });\r\n\r\n    const bookingPaymentRepository = new BookingPaymentRepository();\r\n    const payment = await bookingPaymentRepository.findByExternalIdIncludeBookingUserCredentials(\r\n      data.invoiceId,\r\n      appConfig.type\r\n    );\r\n    if (!payment) throw new HttpCode({ statusCode: 404, message: \"Cal.diy: payment not found\" });\r\n    if (payment.success) return res.status(200).send({ message: \"Payment already registered\" });\r\n    const key = payment.booking?.user?.credentials?.[0].key;\r\n    if (!key) throw new HttpCode({ statusCode: 404, message: \"Cal.diy: credentials not found\" });\r\n\r","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/btcpayserver/api/webhook.ts#L34-L70","documentation":"Authenticity guard on the inbound webhook: the handler reads the signature from the `btcpay-sig`/`BTCPay-Sig` header and requires it to be a present string starting with `sha256=`. If the header is missing, not a string, or lacks the expected prefix, HttpCode 401 is thrown — the request is treated as unauthenticated before any payload parsing.","triggerScenarios":"Webhook delivery where BTCPay Server did not attach the signature header (misconfigured webhook, signature disabled), the header name differs, or a non-BTCPay client posts to the endpoint.","commonSituations":"Webhook recreated in BTCPay without re-copying the secret/signature settings; reverse proxy stripping headers; the endpoint accidentally exposed and probed by scanners; BTCPay Server version that uses a different header casing not covered by the fallback.","solutions":["In BTCPay Server, edit the webhook and confirm the secret is set and that signature delivery is enabled.","Verify no proxy/CDN strips the `btcpay-sig` header (check casing and forwarding rules).","Restrict the endpoint to BTCPay source IPs or require an ingress secret if exposed publicly.","Log the inbound headers (names only) on 401 to confirm whether the header arrived under a different casing."],"exampleFix":"// before\nconst signature = req.headers[\"btcpay-sig\"] || req.headers[\"BTCPay-Sig\"];\nif (!signature || typeof signature !== \"string\" || !signature.startsWith(\"sha256=\"))\n  throw new HttpCode({ statusCode: 401, message: \"Missing or invalid signature format\" });\n\n// after\nconst signature = req.headers[\"btcpay-sig\"];\nif (typeof signature !== \"string\" || !signature.startsWith(\"sha256=\"))\n  throw new HttpCode({ statusCode: 401, message: \"Missing or invalid signature format\" });","handlingStrategy":"validation","validationCode":"const sig = req.headers[\"btcpay-sig\"];\nif (typeof sig !== \"string\" || !sig.startsWith(\"sha256=\")) {\n  return res.status(401).json({ message: \"Missing signature\" });\n}","typeGuard":"function isBTCPaySigHeader(v: unknown): v is string {\n  return typeof v === \"string\" && v.startsWith(\"sha256=\");\n}","tryCatchPattern":"try {\n  handler(req, res);\n} catch (e) {\n  if (e instanceof HttpCode && e.statusCode === 401) {\n    return res.status(401).json({ message: e.message });\n  }\n  throw e;\n}","preventionTips":["Always set a webhook secret when creating the BTCPay webhook.","Ensure proxies forward the `btcpay-sig` header unchanged.","Restrict webhook endpoint ingress to BTCPay IPs where possible.","Log inbound header names (not values) on 401 to debug missing headers cases."],"tags":["webhook","authentication","signature","security","btcpay"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}