Skip to main content
Temporary keys automatically become invalid after a specified time. Unkey handles expiration and cleanup for you, making it easy to implement trial access, time-limited integrations, and security rotation policies.

How expiration works

  1. Set an expiration timestamp — Specify expires as a Unix timestamp in milliseconds
  2. Key works until expiration — Verification succeeds normally until the time is reached
  3. Automatic invalidation — After expiration, verification returns code: EXPIRED
  4. Automatic cleanup — Unkey periodically removes expired keys from the system
Keys without an expires field are permanent and never expire unless manually revoked.

Use cases

Trial access

Give new users a 7-day trial key. It stops working automatically, no manual cleanup needed.

Session-based access

Generate keys that last for a user session (e.g., 24 hours). Perfect for temporary integrations.

Time-limited partnerships

Partner needs API access for a 3-month project. Key expires when the contract ends.

Security rotation

Force key rotation by issuing keys that expire after 90 days.

Creating temporary keys

Basic expiration

# Key expires in 24 hours
EXPIRES=$(($(date +%s) * 1000 + 86400000))

curl -X POST https://api.unkey.com/v2/keys.createKey \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "apiId": "api_...",
    "expires": '$EXPIRES'
  }'

Common expiration durations

const EXPIRATION_TIMES = {
  oneHour: Date.now() + 60 * 60 * 1000,
  oneDay: Date.now() + 24 * 60 * 60 * 1000,
  oneWeek: Date.now() + 7 * 24 * 60 * 60 * 1000,
  oneMonth: Date.now() + 30 * 24 * 60 * 60 * 1000,
  threeMonths: Date.now() + 90 * 24 * 60 * 60 * 1000,
  oneYear: Date.now() + 365 * 24 * 60 * 60 * 1000,
} as const;

// Create a 7-day trial key
const { data } = await unkey.keys.create({
  apiId: "api_...",
  expires: EXPIRATION_TIMES.oneWeek,
});

Specific date expiration

// Expire on a specific date (e.g., end of contract)
const contractEnd = new Date("2026-12-31T23:59:59Z").getTime();

const { data } = await unkey.keys.create({
  apiId: "api_...",
  expires: contractEnd,
  name: "Partner Integration - Q4 2026",
});

Verification response

Expired keys return a specific error code:
{
  "meta": { "requestId": "req_..." },
  "data": {
    "valid": false,
    "code": "EXPIRED",
    "keyId": "key_..."
  }
}
Handle expiration in your application:
import { verifyKey } from "@unkey/api";

try {
  const { meta, data } = await verifyKey({
    key: request.apiKey,
  });

  if (!data.valid) {
    if (data.code === "EXPIRED") {
      return Response.json(
        { error: "Your API key has expired. Please request a new one." },
        { status: 401 }
      );
    }
    return Response.json({ error: data.code }, { status: 401 });
  }

  // Key is valid - continue
} catch (error) {
  console.error(error);
  return Response.json({ error: "Internal error" }, { status: 500 });
}

Managing expiration

Extend expiration

Grant more time to an existing key:
# Extend by 7 more days from now
NEW_EXPIRY=$(($(date +%s) * 1000 + 604800000))

curl -X POST https://api.unkey.com/v2/keys.updateKey \
  -H "Authorization: Bearer $UNKEY_ROOT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "keyId": "key_...",
    "expires": '$NEW_EXPIRY'
  }'

Remove expiration

Make a temporary key permanent:
// Remove expiration - key now lasts forever
await unkey.keys.update({
  keyId: "key_...",
  expires: null,
});

Check time remaining

Calculate how much time a key has left:
export async function getKeyTimeRemaining(keyId: string) {
  const key = await unkey.keys.get({ keyId });

  if (!key.expires) {
    return { expiresAt: null, remaining: Infinity, status: "permanent" };
  }

  const now = Date.now();
  const remaining = key.expires - now;

  if (remaining <= 0) {
    return { expiresAt: key.expires, remaining: 0, status: "expired" };
  }

  return {
    expiresAt: key.expires,
    remaining,
    remainingDays: Math.floor(remaining / (24 * 60 * 60 * 1000)),
    status: "active",
  };
}

Expiration strategies

Trial-to-paid conversion

Grant trial access with automatic expiration, then issue a permanent key on upgrade:
export async function createTrialKey(userId: string) {
  const trialDuration = 7 * 24 * 60 * 60 * 1000; // 7 days

  try {
    const { meta, data } = await unkey.keys.create({
      apiId: "api_...",
      expires: Date.now() + trialDuration,
      credits: {
        remaining: 1000, // Trial also limited to 1000 requests
      },
      name: `Trial - ${userId}`,
      meta: {
        userId,
        tier: "trial",
      },
    });

    // Notify user about trial expiration
    await scheduleEmail({
      to: userId,
      template: "trial-reminder",
      sendAt: data.expires! - 24 * 60 * 60 * 1000, // 1 day before
    });

    return data.key;
  } catch (error) {
    console.error(error);
    throw error;
  }
}

export async function convertToProKey(trialKeyId: string, userId: string) {
  // Revoke trial key
  await unkey.keys.revoke({ keyId: trialKeyId });

  // Issue new unlimited key
  const { data } = await unkey.keys.create({
    apiId: "api_...",
    // No expires field - permanent key
    remaining: 50000,
    refill: {
      interval: "monthly",
      amount: 50000,
    },
    name: `Pro - ${userId}`,
    meta: {
      userId,
      tier: "pro",
    },
  });

  return data.key;
}

Rotating credentials

Force regular key rotation for security:
export async function createRotatingKey(userId: string) {
  const rotationPeriod = 90 * 24 * 60 * 60 * 1000; // 90 days

  const { data } = await unkey.keys.create({
    apiId: "api_...",
    expires: Date.now() + rotationPeriod,
    name: `Rotating Key - ${userId}`,
    meta: {
      userId,
      rotationPolicy: "90-days",
    },
  });

  // Schedule rotation reminder
  await scheduleRotationReminder({
    keyId: data.keyId,
    userId,
    notifyDaysBefore: 14,
  });

  return data.key;
}

// Automatically issue a new key before expiration
export async function autoRotateKey(oldKeyId: string) {
  const oldKey = await unkey.keys.get({ keyId: oldKeyId });

  // Create new key with same configuration
  const { data } = await unkey.keys.create({
    apiId: oldKey.apiId,
    expires: Date.now() + 90 * 24 * 60 * 60 * 1000,
    remaining: oldKey.remaining,
    ratelimits: oldKey.ratelimits,
    name: oldKey.name,
    meta: oldKey.meta,
  });

  // Notify user of new key
  await sendEmail({
    to: oldKey.meta.userId,
    subject: "New API key issued",
    body: `Your old key expires soon. New key: ${data.key}`,
  });

  return data.key;
}

Session-based keys

Generate short-lived keys for authenticated sessions:
export async function createSessionKey(
  userId: string,
  sessionDurationHours: number = 24
) {
  const expires = Date.now() + sessionDurationHours * 60 * 60 * 1000;

  const { data } = await unkey.keys.create({
    apiId: "api_...",
    expires,
    name: `Session - ${userId}`,
    meta: {
      userId,
      type: "session",
      createdAt: new Date().toISOString(),
    },
  });

  return data.key;
}

// Cleanup expired session keys (run as cron job)
export async function cleanupExpiredSessions() {
  const allKeys = await unkey.keys.list({ apiId: "api_..." });
  const now = Date.now();

  const expiredKeys = allKeys.filter(
    (key) => key.meta?.type === "session" && key.expires && key.expires < now
  );

  for (const key of expiredKeys) {
    await unkey.keys.revoke({ keyId: key.keyId });
  }

  console.log(`Cleaned up ${expiredKeys.length} expired session keys`);
}

Partner integrations

Time-limited access for external partners:
export async function createPartnerKey({
  partnerName,
  projectName,
  startDate,
  endDate,
}: {
  partnerName: string;
  projectName: string;
  startDate: Date;
  endDate: Date;
}) {
  const { data } = await unkey.keys.create({
    apiId: "api_...",
    expires: endDate.getTime(),
    name: `${partnerName} - ${projectName}`,
    meta: {
      partnerName,
      projectName,
      startDate: startDate.toISOString(),
      endDate: endDate.toISOString(),
      type: "partner",
    },
  });

  // Log the integration
  await db.partnerIntegrations.create({
    keyId: data.keyId,
    partnerName,
    projectName,
    startDate,
    endDate,
  });

  return data.key;
}

Combining expiration with other limits

Temporary keys work with all other features:
// 7-day trial with multiple constraints
const { data } = await unkey.keys.create({
  apiId: "api_...",
  // Time limit: 7 days
  expires: Date.now() + 7 * 24 * 60 * 60 * 1000,
  // Usage limit: 1000 requests total
  credits: {
    remaining: 1000,
  },
  // Rate limit: Max 100 requests/minute
  ratelimits: [
    {
      name: "requests",
      limit: 100,
      duration: 60000,
      autoApply: true,
    },
  ],
  name: "Trial Key",
});
FeatureExpirationUsage Limits
Limited byTimeRequest count
Expires whenClock hits timestampCredits reach 0
Best forTime-bound access, trialsPay-per-use, quotas
Example”Access for 7 days""1000 requests total”
Use both for robust trials: “7 days OR 1000 requests, whichever comes first.” This prevents abuse while giving flexibility.

Notification strategies

Alert users before keys expire:
export async function checkExpiringKeys() {
  const allKeys = await unkey.keys.list({ apiId: "api_..." });
  const now = Date.now();
  const warningThreshold = 7 * 24 * 60 * 60 * 1000; // 7 days

  for (const key of allKeys) {
    if (!key.expires) continue;

    const timeRemaining = key.expires - now;

    // Send warning if expiring within 7 days
    if (timeRemaining > 0 && timeRemaining < warningThreshold) {
      await sendEmail({
        to: key.meta?.userId,
        subject: "API key expiring soon",
        body: `Your API key "${key.name}" expires in ${Math.floor(
          timeRemaining / (24 * 60 * 60 * 1000)
        )} days.`,
      });
    }
  }
}

// Run daily as a cron job

Best practices

Send reminders at 7 days, 3 days, and 1 day before a key expires. Give users time to renew or rotate.
Production keys should be long-lived or permanent. Expiration is best for temporary access scenarios.
When a key is about to expire, provide a one-click renewal option. Don’t force users to manually create new keys.
Track when keys expire and why. This helps you understand trial conversion rates and rotation compliance.

Next steps

Usage limits

Combine time limits with request quotas

Multi-tenant applications

Manage keys across multiple organizations

Build docs developers (and LLMs) love