API
API keys
Layer mints its own API keys. What a key opens is declared per resource:
each entitlement names a VectorStore,
a Warehouse, or Layer itself, and
carries the scopes and claims for that target. The
ApiKey CRD page covers the resource
model; this page is the REST surface.
You don’t have to mint anything. The default VectorStore credential
(the store key you already own) is accepted as an admin bearer. Minting
starts from it: admin scope is what calls these routes.
Key model
- Tokens look like
hvl_iqGFsDD2PNkyhCqr59jjvKuKL47vqXMz. The raw token is returned once, in the mint response. Layer stores only one-way hashes; a lost token is revoked and re-minted, never recovered. - Every key is an
ApiKeyresource in the cluster —kubectl get apikeysis the audit trail, andkubectl applyis an equal authoring surface (the operator mints and delivers the token via Secret). - Revoking keeps the record; deleting removes it. Propagation takes seconds, not milliseconds.
Routes
| Route | Method | Auth | Behavior |
|---|---|---|---|
/v2/keys | POST | admin | Mint a key. The only response that contains the raw token. |
/v2/keys | GET | admin | List keys — metadata, never material. ?includeRevoked adds revoked and expired keys. |
/v2/keys/{keyId} | GET | admin | One key’s metadata. |
/v2/keys/{keyId}/revoke | POST | admin | Revoke. Idempotent; the record stays. |
/v2/keys/{keyId} | DELETE | admin | Hard delete. |
/v2/keys/authenticate | POST | none | Exchange a raw token for identity and entitlements. |
Admin here means a key with the layer entitlement at admin scope,
or the bootstrap gateway key. authenticate is unauthenticated by
construction — the token is the credential.
Mint
key = await client.mint_key({
"name": "cohort-reader",
"owner": "acme",
"entitlements": {
"vectorstore.prod-turbopuffer": {
"scopes": ["read"],
"namespaces": ["cohort-*"],
},
"warehouse.prod-snowflake": {
"claims": ["notes:cohort:*:read"],
},
},
"expiresAfter": "365d",
})
print(key.token) # shown once, never againkey, err := client.MintKey(ctx, &hevlayer.MintKeyRequest{
Name: "cohort-reader",
Owner: "acme",
Entitlements: hevlayer.ApiKeyEntitlements{
"vectorstore.prod-turbopuffer": {
Scopes: []string{"read"},
Namespaces: []string{"cohort-*"},
},
"warehouse.prod-snowflake": {
Claims: []string{"notes:cohort:*:read"},
},
},
ExpiresAfter: "365d",
})
fmt.Println(key.Token) // shown once, never againconst key = await client.mintKey({
name: "cohort-reader",
owner: "acme",
entitlements: {
"vectorstore.prod-turbopuffer": {
scopes: ["read"],
namespaces: ["cohort-*"],
},
"warehouse.prod-snowflake": {
claims: ["notes:cohort:*:read"],
},
},
expiresAfter: "365d",
});
console.log(key.token); // shown once, never againcurl -X POST "$LAYER_GATEWAY_URL/v2/keys" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "cohort-reader",
"owner": "acme",
"entitlements": {
"vectorstore.prod-turbopuffer": {"scopes": ["read"], "namespaces": ["cohort-*"]},
"warehouse.prod-snowflake": {"claims": ["notes:cohort:*:read"]}
},
"expiresAfter": "365d"
}' The response is 201 Created:
{
"keyId": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
"name": "cohort-reader",
"owner": "acme",
"entitlements": {
"vectorstore.prod-turbopuffer": {"scopes": ["read"], "namespaces": ["cohort-*"]},
"warehouse.prod-snowflake": {"claims": ["notes:cohort:*:read"]}
},
"phase": "Active",
"createdAt": "2026-06-10T00:00:00Z",
"expiresAt": "2027-06-10T00:00:00Z",
"token": "hvl_iqGFsDD2PNkyhCqr59jjvKuKL47vqXMz"
}
| Field | Required | Behavior |
|---|---|---|
name | yes | Unique per install. Becomes the ApiKey resource name. |
owner | no | Free-form owner label, echoed in list and authenticate responses. |
description | no | Free text, shown in listings. |
entitlements | no | Map keyed by target — vectorstore.<name>, warehouse.<name>, or layer. Unknown prefixes are 400s. See the entitlement model. |
expiresAfter | no | Duration or never. Defaults to 365d; expiresAt is computed at mint. |
Validation failures are 400s with field-level messages; nothing is
created on a failed mint. An entitlement whose target does not exist is
accepted — it grants nothing and surfaces as an
EntitlementTargetMissing condition on the resource, so keys and
targets can be applied in either order.
Mint an admin key the same way — "entitlements": {"layer": {"scopes": ["admin"]}}. An admin key can mint further keys; key custody is the
only hierarchy.
List and get
keys = await client.list_keys()
key = await client.get_key("0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9")keys, err := client.ListKeys(ctx, nil)
key, err := client.GetKey(ctx, "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9")const keys = await client.listKeys();
const key = await client.getKey("0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9");curl "$LAYER_GATEWAY_URL/v2/keys" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" {
"keys": [
{
"keyId": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
"name": "cohort-reader",
"owner": "acme",
"entitlements": {
"vectorstore.prod-turbopuffer": {"scopes": ["read"], "namespaces": ["cohort-*"]},
"warehouse.prod-snowflake": {"claims": ["notes:cohort:*:read"]}
},
"phase": "Active",
"createdAt": "2026-06-10T00:00:00Z",
"expiresAt": "2027-06-10T00:00:00Z",
"lastSeenAt": "2026-06-10T12:00:00Z"
}
]
}
Listings carry metadata only — never tokens or recoverable hashes.
lastSeenAt advances at most once per five minutes per key.
Authenticate
External systems present a raw token and get back keyId — a stable
actor id — plus the full entitlements map, then make their own
authorization decisions from the claims. This is the verb that makes
Layer a key store for applications that keep authorization to
themselves: Layer never interprets a claim string.
identity = await client.authenticate_key({"token": presented})
claims = identity.entitlements["warehouse.prod-snowflake"].claimsidentity, err := client.AuthenticateKey(ctx, &hevlayer.AuthenticateKeyRequest{
Token: presented,
})
claims := identity.Entitlements["warehouse.prod-snowflake"].Claimsconst identity = await client.authenticateKey({ token: presented });
const claims = identity.entitlements["warehouse.prod-snowflake"].claims;curl -X POST "$LAYER_GATEWAY_URL/v2/keys/authenticate" \
-H "Content-Type: application/json" \
-d '{"token": "hvl_iqGFsDD2PNkyhCqr59jjvKuKL47vqXMz"}' 200 returns {keyId, name, owner, entitlements, expiresAt}. Invalid,
revoked, and expired tokens all answer 401 indistinguishably, in
constant time on a miss. The route is rate-limited.
Revoke and delete
await client.revoke_key("0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9")
await client.delete_key("0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9")_, err := client.RevokeKey(ctx, "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9")
_, err = client.DeleteKey(ctx, "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9")await client.revokeKey("0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9");
await client.deleteKey("0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9");curl -X POST "$LAYER_GATEWAY_URL/v2/keys/$KEY_ID/revoke" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" The gateway stops accepting a revoked key within seconds. Rotation is
mint-new, deploy, revoke-old — there is no in-place rotation. Revoked
keys stay in ?includeRevoked listings for audit; delete when the
record itself should go.
Using a minted key
A minted key works anywhere its entitlements reach. A
vectorstore.<name> entitlement opens data-plane routes against that
store, inside its namespace globs:
curl "$LAYER_GATEWAY_URL/v2/namespaces/cohort-7/query" \
-X POST \
-H "Authorization: Bearer hvl_iqGFsDD2PNkyhCqr59jjvKuKL47vqXMz" \
-H "Content-Type: application/json" \
-d '{"rank_by": ["text", "BM25", "acme"], "top_k": 10}'
Outside the grant, the gateway answers 403:
{"error": "namespace not in key grant", "namespace": "orders"}
A request that needs a higher scope answers 403 with the scope named:
{"error": "insufficient API key scope", "required_scope": "admin"}
A key whose entitlements carry only claims — no scopes — is a pure external-store key: it authenticates, but opens no Layer route.
Unlike the store key, a minted key is gateway-only — it cannot be used against the upstream store directly, so the SDK clients’ direct fall-through path is unavailable to it and fails fast.
kubectl
The CRD is the other authoring surface — apply an ApiKey with no
credential and the operator mints the token into a Secret named by
status.secretRef:
kubectl apply -f key.yaml
kubectl get apikeys -n layer
kubectl get secret apikey-cohort-reader -n layer -o jsonpath='{.data.token}' | base64 -d
Both surfaces round-trip through one schema — kubectl get apikey -o yaml and GET /v2/keys/{keyId} are two spellings of the same object.
The ApiKey CRD page has the full
resource model and the Kubernetes RBAC the chart ships for it.
CLI
The same operations from the CLI:
layer keys mint cohort-reader --owner acme \
--entitle vectorstore.prod-turbopuffer=read \
--namespaces "cohort-*" \
--claim warehouse.prod-snowflake="notes:cohort:*:read"
layer keys ls
layer keys revoke cohort-reader
layer keys mint prints the token once, alone on stdout for piping;
the metadata table goes to stderr.