calcom/cal.diy · error · HttpCode
Cal.diy: Invalid BTCPay credentials
Error message
Cal.diy: Invalid BTCPay credentials
What it means
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.
Source
Thrown at packages/app-store/btcpayserver/api/webhook.ts:73
if (!webhookData.success) return res.status(400).json({ message: "Invalid webhook payload" });
const data = webhookData.data;
if (!SUPPORTED_INVOICE_EVENTS.includes(data.type))
return res.status(200).send({ message: "Webhook received but ignored" });
const bookingPaymentRepository = new BookingPaymentRepository();
const payment = await bookingPaymentRepository.findByExternalIdIncludeBookingUserCredentials(
data.invoiceId,
appConfig.type
);
if (!payment) throw new HttpCode({ statusCode: 404, message: "Cal.diy: payment not found" });
if (payment.success) return res.status(200).send({ message: "Payment already registered" });
const key = payment.booking?.user?.credentials?.[0].key;
if (!key) throw new HttpCode({ statusCode: 404, message: "Cal.diy: credentials not found" });
const parsedKey = btcpayCredentialKeysSchema.safeParse(key);
if (!parsedKey.success)
throw new HttpCode({ statusCode: 400, message: "Cal.diy: Invalid BTCPay credentials" });
const { webhookSecret, storeId } = parsedKey.data;
if (storeId !== data.storeId)
throw new HttpCode({ statusCode: 400, message: "Cal.diy: Store ID mismatch" });
const expectedSignature = signature.split("=")[1];
const computedSignature = verifyBTCPaySignature(rawBody, expectedSignature, webhookSecret);
if (computedSignature.length !== expectedSignature.length) {
throw new HttpCode({ statusCode: 400, message: "signature mismatch" });
}
const isValid = crypto.timingSafeEqual(
Buffer.from(computedSignature, "hex"),
Buffer.from(expectedSignature, "hex")
);
if (!isValid) throw new HttpCode({ statusCode: 400, message: "signature mismatch" });
const traceContext = distributedTracing.createTrace("btcpayserver_webhook", {
View on GitHub (pinned to 176037d0af)
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.
Example fix
// before
const parsedKey = btcpayCredentialKeysSchema.safeParse(key);
if (!parsedKey.success)
throw new HttpCode({ statusCode: 400, message: "Cal.diy: Invalid BTCPay credentials" });
// after
const parsedKey = btcpayCredentialKeysSchema.safeParse(key);
if (!parsedKey.success) {
const issues = parsedKey.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
log.warn("BTCPay credential schema rejected", { issues });
throw new HttpCode({ statusCode: 400, message: `Cal.diy: Invalid BTCPay credentials (${issues})` });
} Defensive patterns
Strategy: validation
Validate before calling
const parsedKey = btcpayCredentialKeysSchema.safeParse(key);
if (!parsedKey.success) {
const issues = parsedKey.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
return res.status(400).json({ message: `Invalid credentials: ${issues}` });
} Type guard
import { btcpayCredentialKeysSchema } from "../lib/btcpayCredentialKeysSchema";
function isBTCPayCredentialKey(k: unknown) {
return btcpayCredentialKeysSchema.safeParse(k).success;
} Try / catch
try {
useCredential(key);
} catch (e) {
if (/Invalid BTCPay credentials/.test(String((e as Error).message))) {
await flagCredentialForReconnection(credentialId);
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Invalid credentials for userId ${credential.userId} and appI
- ${appSlug} not connected.
- parsedQuery.error.message
- Invalid refreshed tokens were returned
- Bad Request
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/fbd80f141a2678ac.
Report an issue: GitHub.