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
- Send only one of the two fields; delete the other from the payload before POST.
- On the client, make the two fields mutually exclusive (radio group) so the unused one is omitted from the body.
- 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
- Use a mutually-exclusive UI control so the unused field is omitted entirely.
- Build the payload explicitly from the chosen option, not from a form dump.
- Unit-test the XOR guard at the input-service boundary.
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
- Either expiresAt or maxUsageCount must be provided
- ApiKeysService -Cannot set both apiKeyDaysValid and apiKeyNe
- teamId is required for team events, please provide a valid t
- username is required for non-team events, please provide a v
- 'to' must not be before 'from'
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/029fdf7a58cff3f1.
Report an issue: GitHub.