calcom/cal.diy · error · Error

Either expiresAt or maxUsageCount must be provided

Error message

Either expiresAt or maxUsageCount must be provided

What it means

Thrown by PrivateLinksInputService.transformCreateInput. A private link must be constrained by either a time bound (expiresAt) or a usage bound (maxUsageCount): exactly one of the two. If neither is present in the CreatePrivateLinkInput, a plain Error('Either expiresAt or maxUsageCount must be provided') is thrown; this then surfaces through the service's catch block as a 400. The guard enforces that no private link can be both unbounded and eternal.

Source

Thrown at apps/api/v2/src/platform/event-types-private-links/services/private-links-input.service.ts:14

import { Injectable } from "@nestjs/common";

import { CreatePrivateLinkInput, UpdatePrivateLinkInput } from "@calcom/platform-types";

@Injectable()
export class PrivateLinksInputService {
  constructor() {}

  transformCreateInput(input: CreatePrivateLinkInput): CreatePrivateLinkInput {
    const hasExpires = input.expiresAt !== undefined && input.expiresAt !== null;
    const hasMaxCount = typeof input.maxUsageCount === "number";

    if (!hasExpires && !hasMaxCount) {
      throw new Error("Either expiresAt or maxUsageCount must be provided");
    }

    if (hasExpires && hasMaxCount) {
      throw new Error("Provide only one of expiresAt or maxUsageCount");
    }

    return {
      expiresAt: input.expiresAt,
      maxUsageCount: input.maxUsageCount ?? (hasMaxCount ? input.maxUsageCount : undefined),
    };
  }

  transformUpdateInput(input: UpdatePrivateLinkInput): UpdatePrivateLinkInput {
    return {
      linkId: input.linkId,
      expiresAt: input.expiresAt,
      maxUsageCount: input.maxUsageCount,
    };

View on GitHub (pinned to 176037d0af)

Solutions

  1. Send exactly one of { expiresAt: <iso8601|null> } or { maxUsageCount: <number> } in the request body.
  2. If you want a usage cap, ensure maxUsageCount is a JSON number, not a string.
  3. On the client, disable submit until one of the two fields is filled.

Example fix

// before
await api.post(`/v2/event-types/${id}/private-links`, {});

// after
await api.post(`/v2/event-types/${id}/private-links`, { maxUsageCount: 5 });
// or
await api.post(`/v2/event-types/${id}/private-links`, { expiresAt: '2025-12-31T23:59:59Z' });
Defensive patterns

Strategy: validation

Validate before calling

function validateCreateInput(input: { expiresAt?: string | null; maxUsageCount?: number }) {
  const hasExpires = input.expiresAt != null;
  const hasMax = typeof input.maxUsageCount === 'number';
  if (!hasExpires && !hasMax) throw new Error('Provide expiresAt or maxUsageCount');
  if (hasExpires && hasMax) throw new Error('Provide only one');
  return input;
}

Type guard

function isValidCreateInput(i: unknown): i is { expiresAt?: string } | { maxUsageCount: number } {
  if (typeof i !== 'object' || i === null) return false;
  const o = i as Record<string, unknown>;
  const hasExpires = o.expiresAt != null;
  const hasMax = typeof o.maxUsageCount === 'number';
  return (hasExpires && !hasMax) || (!hasExpires && hasMax);
}

Try / catch

try {
  await privateLinksService.createPrivateLink(eventTypeId, userId, input);
} catch (e) {
  if (e instanceof BadRequestException && /expiresAt|maxUsageCount/.test(e.message)) {
    // fix the payload: send exactly one of the two
  } else throw e;
}

Prevention

When it happens

Trigger: POST /v2/event-types/{id}/private-links with a body like {} or {expiresAt: null, maxUsageCount: undefined}; a client that constructs the input from optional form fields where both were left blank; maxUsageCount passed as a string (typeof !== 'number' so hasMaxCount is false) alongside a null expiresAt.

Common situations: Frontend 'create private link' form submitted with both fields empty; maxUsageCount sent as a string '5' rather than the number 5 (typeof '5' === 'string', so the guard doesn't see it); a default value was removed during a refactor leaving both undefined.

Related errors


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