API keys

An API key lets a backend call Gate's own /api/v1 endpoints without a user session. It is the right credential for reading configuration at boot, recording usage, checking entitlements, or running checkout from a worker.

It is the wrong credential for representing a user — for that, register an OAuth client. An API key also never administers Gate itself: creating keys, changing project settings, and every organization route need a person.

Key format

Text
gate_sk_<12 alphanumeric>_<43 base64url>
        └── prefix ──┘   └─── secret ───┘

The prefix is stored in the clear and shown in the dashboard, so you can tell keys apart. The whole key is hashed with HMAC-SHA256 and compared in constant time. Gate never stores the key itself, so it cannot be recovered — only replaced.

Create one

Open the project's API keys tab and choose Create API key. Give it a name, optionally limit it to one environment, and pick its scopes. Owners and admins can create keys.

The same call over HTTP requires a user session with credentials:manage:

const response = await fetch(
  `https://evonia-gate.annatarhe.com/api/v1/projects/${projectId}/api-keys`,
  {
    method: "POST",
    headers: { "content-type": "application/json", cookie: request.headers.get("cookie")! },
    body: JSON.stringify({
      name: "ShellTime API",
      description: "Reads configuration at boot and records usage",
      environmentId, // omit to allow every environment
      scopes: ["config:read", "usage:write"],
      expiresAt: "2027-01-01T00:00:00.000Z", // optional
    }),
  },
);

const { data } = await response.json();
// data.secret exists only in THIS response.
await secretStore.write("GATE_API_KEY", data.secret);

Constraints:

  • name is 2–80 characters; description is optional, up to 500.
  • scopes holds at least one of the API key scopes.
  • environmentId, when given, must belong to the project.
  • expiresAt is an optional ISO-8601 timestamp.

Use it

Send the key as X-API-Key. Do not put it in a query string.

const response = await fetch(
  `https://evonia-gate.annatarhe.com/api/v1/environments/${environmentId}/configs`,
  {
    headers: {
      "x-api-key": process.env.GATE_API_KEY!,
      "x-request-id": crypto.randomUUID(),
    },
  },
);

Scopes

Every key can read its own project and environments. Everything else needs a scope:

ScopeAllows
config:readRead configuration. API keys also receive secret values.
config:writeCreate, update, archive, and restore configuration.
usage:readRead usage analytics.
usage:writeRecord usage events.
billing:readRead Stripe settings, plans, subscriptions, and entitlements.
billing:writeManage Stripe settings and plans, start checkout, cancel, and grant entitlements.
users:writeRemove, restore, or delete a product's end users.
*Every API key scope.

Grant the narrowest set that works:

JobScopes
Read configuration at bootconfig:read
Record usage eventsusage:write
Read analyticsusage:read, billing:read
Check entitlementsbilling:read
Run checkout and grantsbilling:write
Remove or delete end usersusers:write

* grants every scope above, and nothing more: it never lets a key change project settings, manage credentials, or reach another project.

Environments

A key with an environment set is confined to it. Any request touching a different one fails with 403 API key is limited to another environment — before the scope check runs.

Limit production keys to production. A key for all environments that leaks from a staging host can read production secrets.

Rotation

There is no rotate endpoint. Roll forward:

  1. Create a second key with the same scopes.

  2. Deploy the new key to every consumer.

  3. Confirm the old key is idle — its last use stops advancing.

  4. Revoke the old key from the API keys tab, or with POST /projects/{projectId}/api-keys/{keyId}/revoke.

Revoking takes effect immediately and cannot be undone. Set expiresAt on creation if you want a deadline that enforces itself.