calcom/cal.diy · error · HttpCode

Cal.diy: Store ID mismatch

Error message

Cal.diy: Store ID mismatch

What it means

The webhook payload's `storeId` is compared against the `storeId` stored in the credential. A mismatch means the event came from a different BTCPay store than the one this user configured, so it is rejected with HttpCode 400 to prevent cross-store payment attribution.

Source

Thrown at packages/app-store/btcpayserver/api/webhook.ts:77

      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", {
      meta: { paymentId: payment.id, bookingId: payment.bookingId },
    });
    await handlePaymentSuccess({
      paymentId: payment.id,

View on GitHub (pinned to 176037d0af)

Solutions

  1. Open the BTCPay integration settings in Cal.diy and update the store id to match the store that owns the invoice.
  2. Confirm the BTCPay webhook is scoped to a single store, not server-wide.
  3. If multiple stores must be supported, configure separate credentials/webhook endpoints per store.
  4. Compare `data.storeId` from the payload against the value shown in BTCPay's store settings to identify the drift.
Defensive patterns

Strategy: validation

Validate before calling

if (storeId !== data.storeId) {
  return res.status(400).json({ message: `Store mismatch: webhook=${data.storeId} credential=${storeId}` });
}

Try / catch

try {
  processWebhook(data, credential);
} catch (e) {
  if (e instanceof HttpCode && /Store ID mismatch/.test(e.message)) {
    return res.status(400).json({ message: e.message });
  }
  throw e;
}

Prevention

When it happens

Trigger: A single BTCPay Server has multiple stores and the webhook endpoint receives events from a store other than the one linked to the user's credential; the user reconfigured their store id in BTCPay without updating the Cal.diy credential; webhook URL shared across stores.

Common situations: Multi-store BTCPay accounts; webhook configured at server level rather than per-store; copy-paste error when entering the store id during setup; store recreated with a new id after deletion.

Related errors


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