Service API keys
A service 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, or managing billing from a
worker.
It is the wrong credential for representing a user. Endpoints that mint credentials reject it outright.
Key format
gate_sk_<12 alphanumeric>_<43 base64url>
└── prefix ──┘ └─── secret ───┘The prefix is stored in the clear and used to look up the row; 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
Requires a user session with credential:manage. A service account cannot create another service
account.
const response = await fetch(
`https://evonia-gate.annatarhe.com/api/v1/projects/${projectId}/service-accounts`,
{
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.apiKey exists only in THIS response.
await secretStore.write("GATE_API_KEY", data.apiKey);curl -sS -X POST \
"https://evonia-gate.annatarhe.com/api/v1/projects/$PROJECT_ID/service-accounts" \
-H "content-type: application/json" \
-b "better-auth.session_token=$SESSION" \
-d '{
"name": "ShellTime API",
"description": "Reads configuration at boot and records usage",
"scopes": ["config:read", "usage:write"]
}'Constraints:
nameis 2–80 characters;descriptionis optional, up to 500.scopesholds 1–30 entries, each matching^[a-z*][a-z0-9:*_-]*$.environmentId, when given, must belong to the project.expiresAtis 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(),
},
},
);curl -sS "https://evonia-gate.annatarhe.com/api/v1/environments/$ENVIRONMENT_ID/configs" \
-H "x-api-key: $GATE_API_KEY"req.Header.Set("X-API-Key", os.Getenv("GATE_API_KEY"))request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")Scopes
Scopes are control-plane permission strings, checked directly against what an endpoint requires. A key
with config:read may call GET /environments/{id}/configs; the same key gets 403 from
PUT /environments/{id}/configs/{key}, which needs config:update.
* is a wildcard matching everything. Use it only for a trusted administrative worker.
Grant the narrowest set that works:
| Job | Scopes |
|---|---|
| Read configuration at boot | config:read |
| Record usage events | usage:write |
| Read analytics | usage:read, billing:read |
| Manage plans and checkout | billing:manage |
| Ask authorization questions | project:read |
Environment pinning
A service account with environmentId set is confined to that environment. Any request touching a
different one fails with 403 Service account is scoped to another environment — before the
permission check runs.
Pin production keys. An unpinned key that leaks from a staging host can read production secrets.
What a service key cannot do
| Refused | Why |
|---|---|
Every /organizations/* endpoint | Requires a user session |
| Create or disable service accounts | Requires a user session |
| Create, list, or disable OIDC clients | Requires a user session |
| Write the Stripe connection | Requires a user session |
| Reach another project | The key is bound to one project |
The rejection is 403 A user session is required.
Rotation
There is no rotate endpoint. Roll forward:
-
Create a second service account with the same scopes.
-
Deploy the new key to every consumer.
-
Confirm the old key is idle — its
lastUsedAtstops advancing. -
Disable the old service account with
POST /projects/{projectId}/service-accounts/{accountId}/disable.
Disabling the service account invalidates its keys immediately. Set expiresAt on creation if you
want a deadline that enforces itself.