{"record":{"id":"fbd80f141a2678ac","repo":"calcom/cal.diy","slug":"cal-diy-invalid-btcpay-credentials","errorCode":null,"errorMessage":"Cal.diy: Invalid BTCPay credentials","messagePattern":"Cal\\.diy: Invalid BTCPay credentials","errorType":"http","errorClass":"HttpCode","httpStatus":400,"severity":"error","filePath":"packages/app-store/btcpayserver/api/webhook.ts","lineNumber":73,"sourceCode":"    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\n    const parsedKey = btcpayCredentialKeysSchema.safeParse(key);\r\n    if (!parsedKey.success)\r\n      throw new HttpCode({ statusCode: 400, message: \"Cal.diy: Invalid BTCPay credentials\" });\r\n\r\n    const { webhookSecret, storeId } = parsedKey.data;\r\n    if (storeId !== data.storeId)\r\n      throw new HttpCode({ statusCode: 400, message: \"Cal.diy: Store ID mismatch\" });\r\n\r\n    const expectedSignature = signature.split(\"=\")[1];\r\n    const computedSignature = verifyBTCPaySignature(rawBody, expectedSignature, webhookSecret);\r\n\r\n    if (computedSignature.length !== expectedSignature.length) {\r\n      throw new HttpCode({ statusCode: 400, message: \"signature mismatch\" });\r\n    }\r\n    const isValid = crypto.timingSafeEqual(\r\n      Buffer.from(computedSignature, \"hex\"),\r\n      Buffer.from(expectedSignature, \"hex\")\r\n    );\r\n    if (!isValid) throw new HttpCode({ statusCode: 400, message: \"signature mismatch\" });\r\n\r\n    const traceContext = distributedTracing.createTrace(\"btcpayserver_webhook\", {\r","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/btcpayserver/api/webhook.ts#L55-L91","documentation":"The stored credential `key` is parsed against `btcpayCredentialKeysSchema` (`{ serverUrl: URL string, storeId, apiKey, webhookSecret }`). If parsing fails — missing fields, `serverUrl` not a valid URL, or wrong shape — HttpCode 400 is thrown because the webhook secret needed for signature verification cannot be safely extracted.","triggerScenarios":"Credential `key` was written in an older/different schema (e.g. missing `webhookSecret` or `apiKey`), the URL field contains a non-URL string, partial credential saved after a failed setup, or schema was extended without migrating existing rows.","commonSituations":"App version upgrade that added `webhookSecret`/`storeId` to the schema but didn't migrate existing credentials; user saved credentials mid-setup; manual DB edits corrupting the JSON.","solutions":["Open the affected user's BTCPay credential and re-enter server URL, store id, API key, and webhook secret through the integration UI.","Check `parsedKey.error.issues` (log it server-side) to see exactly which field fails.","Run a migration/backfill to bring legacy credential `key` JSON up to the current schema.","Add a setup-time validation step so incomplete credentials cannot be persisted."],"exampleFix":"// before\nconst parsedKey = btcpayCredentialKeysSchema.safeParse(key);\nif (!parsedKey.success)\n  throw new HttpCode({ statusCode: 400, message: \"Cal.diy: Invalid BTCPay credentials\" });\n\n// after\nconst parsedKey = btcpayCredentialKeysSchema.safeParse(key);\nif (!parsedKey.success) {\n  const issues = parsedKey.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`).join(\"; \");\n  log.warn(\"BTCPay credential schema rejected\", { issues });\n  throw new HttpCode({ statusCode: 400, message: `Cal.diy: Invalid BTCPay credentials (${issues})` });\n}","handlingStrategy":"validation","validationCode":"const parsedKey = btcpayCredentialKeysSchema.safeParse(key);\nif (!parsedKey.success) {\n  const issues = parsedKey.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`).join(\"; \");\n  return res.status(400).json({ message: `Invalid credentials: ${issues}` });\n}","typeGuard":"import { btcpayCredentialKeysSchema } from \"../lib/btcpayCredentialKeysSchema\";\nfunction isBTCPayCredentialKey(k: unknown) {\n  return btcpayCredentialKeysSchema.safeParse(k).success;\n}","tryCatchPattern":"try {\n  useCredential(key);\n} catch (e) {\n  if (/Invalid BTCPay credentials/.test(String((e as Error).message))) {\n    await flagCredentialForReconnection(credentialId);\n  }\n  throw e;\n}","preventionTips":["Validate the credential shape at setup time before persisting.","Run a backfill migration when extending the schema.","Surface zod issue details server-side for faster triage.","Add a unit test that the schema accepts current BTCPay credential JSON."],"tags":["validation","zod","credentials","schema","btcpay"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}