calcom/cal.diy · error · Error

Provide only one of expiresAt or maxUsageCount

Error message

Provide only one of expiresAt or maxUsageCount

What it means

The companion guard in PrivateLinksInputService.transformCreateInput: if both expiresAt and maxUsageCount are supplied, Error('Provide only one of expiresAt or maxUsageCount') is thrown. A private link may be time-limited OR usage-limited, not both, so the input must be exclusive. This prevents ambiguous semantics (does it die at expiry or at the Nth use?) and avoids the awkward return expression at line 23 that would otherwise produce an inconsistent shape.

Source

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

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 only one of the two fields; delete the other from the payload before POST.
  2. On the client, make the two fields mutually exclusive (radio group) so the unused one is omitted from the body.
  3. If importing many links, branch on which bound applies and build the payload accordingly.

Example fix

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

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

Strategy: validation

Validate before calling

function sanitizeCreateInput(input: { expiresAt?: string | null; maxUsageCount?: number }) {
  const hasExpires = input.expiresAt != null;
  const hasMax = typeof input.maxUsageCount === 'number';
  if (hasExpires && hasMax) delete input.maxUsageCount; // or throw, per UX
  return input;
}

Type guard

function isExclusiveInput(i: unknown): boolean {
  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;
}

Try / catch

try {
  await privateLinksService.createPrivateLink(eventTypeId, userId, input);
} catch (e) {
  if (e instanceof BadRequestException && e.message === 'Provide only one of expiresAt or maxUsageCount') {
    // strip one field and retry
  } else throw e;
}

Prevention

When it happens

Trigger: POSTing { expiresAt: '...', maxUsageCount: 5 } to /v2/event-types/{id}/private-links; a form that defaults both fields to a value and submits without clearing the unused one; serializing a draft object that happened to populate both.

Common situations: UI that lets the user pick 'expire on' OR 'after N uses' but submits the union of both; migration/import script that fills both columns; copy-paste from another link's payload.

Related errors


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