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

  1. Open the affected user's BTCPay credential and re-enter server URL, store id, API key, and webhook secret through the integration UI.
  2. Check `parsedKey.error.issues` (log it server-side) to see exactly which field fails.
  3. Run a migration/backfill to bring legacy credential `key` JSON up to the current schema.
  4. 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

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


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/fbd80f141a2678ac. Report an issue: GitHub.