Blog
3 min read

How to Generate and Manage API Keys for Your Users

Letting customers call your API means issuing API keys. How to generate secure keys, prefixes that make them identifiable and scannable, storing only a hash, showing the key once, scopes and expiry, rotation, rate limits per key, logging usage, and revoking keys.

When your customers want to call your API from their own code — or connect your product to Zapier, an AI agent or a script — you need to give them API keys. Getting this right is mostly about a few careful decisions. (What is an API key?, API authentication methods)

1. Generate keys properly

Use a cryptographically secure random generator, with enough randomness (at least 128 bits, commonly 256):

import { randomBytes } from 'node:crypto'

function generateKey() {
  const secret = randomBytes(32).toString('base64url')   // 256 bits
  return `sk_live_${secret}`
}

Never use Math.random(), UUIDv4 alone for long-lived secrets, timestamps, or anything derived from user data.

2. Add a prefix

Prefixes like sk_live_, sk_test_, or your product name (acme_sk_) help:

  • users (and your support team) see what kind of key it is,
  • you separate test and live environments,
  • secret scanners — including GitHub's — can detect your keys if they're leaked. You can register your key format with GitHub's secret scanning partner programme. (GitHub secret scanning)

3. Store only a hash

Treat API keys like passwords: store a hash, never the key itself.

import { createHash } from 'node:crypto'

const hash = createHash('sha256').update(key).digest('hex')
await db.apiKey.create({
  data: {
    userId,
    name: 'Zapier integration',
    prefix: key.slice(0, 12),       // for display: "sk_live_Ab3x…"
    hash,
    scopes: ['invoices:read'],
    expiresAt: null,
  },
})

A fast hash like SHA-256 is fine here (unlike passwords) because keys are long and random — there's nothing to brute-force. If your database leaks, attackers get hashes, not working keys. (Hashing vs encryption, Password hashing)

4. Show the key once

Display the full key once, at creation, with a clear "copy it now — you won't see it again". Afterwards show only the prefix and last-used date. Lost key? Create a new one.

5. Look up and verify

async function authenticate(req) {
  const key = req.headers.authorization?.replace(/^Bearer /, '')
  if (!key) return null
  const hash = createHash('sha256').update(key).digest('hex')
  const record = await db.apiKey.findUnique({ where: { hash } })
  if (!record || record.revokedAt || (record.expiresAt && record.expiresAt < new Date())) return null
  void db.apiKey.update({ where: { id: record.id }, data: { lastUsedAt: new Date() } })
  return record
}

Accept keys in a header (Authorization: Bearer … or X-API-Key), never in the URL — URLs end up in logs. (Query parameters explained)

6. Scopes

Let users create keys that can do only what they need: invoices:read vs invoices:write. A key for a read-only dashboard shouldn't be able to delete data. Check the scope on every endpoint. (Principle of least privilege, Role-based access control)

Keys belong to an account or workspace, and every query must still be limited to that account's data. (IDOR explained)

7. Rate limits per key

Limit requests per key (and per account), return 429 with Retry-After, and document the limits. (Implementing rate limiting)

8. Rotation and revocation

  • Users can revoke any key instantly.
  • Allow multiple active keys so users can rotate without downtime: create new, deploy it, revoke old.
  • Optional expiry dates, and reminders before expiry.
  • Revoke automatically if a scanner reports a key leaked.

9. Log usage

Record which key made which request (key ID, never the key), so users can see activity and you can investigate abuse. (Structured logging)

10. Test vs live

Separate test keys that only touch test data make integrations safe to build — the approach Stripe popularised.

The UI checklist

  • Name each key ("Zapier", "CI").
  • Choose scopes and optional expiry.
  • Show once; copy button.
  • List with prefix, created, last used.
  • Revoke button with confirmation.

EasySpawn gives your API a server and Postgres database to store hashed keys and usage logs, with Claude Code to build the key management screens and middleware. See how it works or join the waitlist.

Related: What Is an API Key? · API Authentication Methods · Implementing Rate Limiting · GitHub Secret Scanning

Keep reading