vercel/ai · error · Error

Cartesia realtime client secrets must expire between 1 and 3

Error message

Cartesia realtime client secrets must expire between 1 and 3600 seconds.

What it means

doCreateClientSecret validates options.expiresAfterSeconds client-side before requesting an access token. If the value is provided but is not a positive integer within 1–3600, a plain Error is thrown. This mirrors the Cartesia API's own constraint on client-secret lifetimes.

Source

Thrown at packages/cartesia/src/cartesia-realtime-model.ts:77

  constructor(
    modelId: CartesiaRealtimeModelId,
    config: CartesiaRealtimeModelConfig,
  ) {
    this.modelId = modelId;
    this.provider = config.provider;
    this.config = config;
  }

  async doCreateClientSecret(
    options: RealtimeModelV4ClientSecretOptions,
  ): Promise<RealtimeModelV4ClientSecretResult> {
    const expiresIn = options.expiresAfterSeconds;
    if (
      expiresIn != null &&
      (!Number.isInteger(expiresIn) || expiresIn <= 0 || expiresIn > 3600)
    ) {
      throw new Error(
        'Cartesia realtime client secrets must expire between 1 and 3600 seconds.',
      );
    }

    const sessionConfig = options.sessionConfig;
    const language = sessionConfig?.inputAudioTranscription?.language;
    if (language != null && language !== 'en') {
      throw new Error('Cartesia Ink 2 currently supports English only.');
    }

    const fetchFn = this.config.fetch ?? fetch;
    const response = await fetchFn(`${this.config.baseURL}/access-token`, {
      method: 'POST',
      headers: {
        ...this.config.headers(),
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Set expiresAfterSeconds to an integer between 1 and 3600, or omit the option entirely for the default expiry.
  2. Clamp/validate the value before calling: Math.min(3600, Math.max(1, seconds)).
  3. If 0 was intended as 'no expiry', remove the expiresAfterSeconds field instead.

Example fix

// before
const secret = await model.doCreateClientSecret({ expiresAfterSeconds: 7200 });

// after
const secret = await model.doCreateClientSecret({ expiresAfterSeconds: 3600 });
Defensive patterns

Strategy: validation

Validate before calling

function isValidExpiry(s: unknown): boolean {
  return s == null || (Number.isInteger(s) && s > 0 && s <= 3600);
}
// call before requesting the secret
if (!isValidExpiry(opts.expiresAfterSeconds)) throw new RangeError('expiresAfterSeconds must be 1-3600 or omitted');

Type guard

function isValidExpirySeconds(v: unknown): v is number | undefined {
  return v === undefined || (typeof v === 'number' && Number.isInteger(v) && v > 0 && v <= 3600);
}

Try / catch

try {
  const secret = await model.doCreateClientSecret(options);
} catch (e) {
  if (e instanceof RangeError || /1 and 3600 seconds/.test(String(e.message))) {
    // clamp and retry once
    const clamped = Math.min(3600, Math.max(1, Math.floor(Number(options.expiresAfterSeconds) || 3600)));
    return model.doCreateClientSecret({ ...options, expiresAfterSeconds: clamped });
  }
  throw e;
}

Prevention

When it happens

Trigger: cartesia.realtime(modelId).doCreateClientSecret({ expiresAfterSeconds: 0 | -5 | 4000 | 12.5 | ... }) — any non-integer, non-positive, or >3600 value.

Common situations: Computing the expiry in minutes and passing seconds incorrectly (e.g. 7200 instead of 3600); passing 0 thinking it means 'no expiry' instead of omitting the field; passing a string parsed value that isn't an integer.

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/d9827624491b1321. Report an issue: GitHub.