# qURL Credentials and Authentication

> The three qURL credential types — API key, enrollment token, and device credential — what each one proves, how to create them, and what scopes mean.

Source: https://layerv.ai/docs/credentials/

---

API reference guide

# Credentials and authentication.

qURL has three credentials, and you only ever manage one of them. This page explains what each one proves, how to create the two you can create, and what scopes decide — which is permissions, and only permissions.

[See the three types](https://layerv.ai/docs/credentials/#types) [Which one do I want?](https://layerv.ai/docs/credentials/#choose)

All examples call `https://api.layerv.ai/v1`.

[The three types](https://layerv.ai/docs/credentials/#types) [The proof rule](https://layerv.ai/docs/credentials/#proof) [Creating credentials](https://layerv.ai/docs/credentials/#create) [Scopes](https://layerv.ai/docs/credentials/#scopes) [Which one do I want?](https://layerv.ai/docs/credentials/#choose) [The create request](https://layerv.ai/docs/credentials/#request-shape) [Errors](https://layerv.ai/docs/credentials/#errors)

The model

## One credential you manage. Two the platform hands you.

Every credential in your account reports its `kind`. That field is the whole model: it tells you what the credential is, how long it lives, and who was allowed to create it.

01 

### API key — `kind: api_key`

The durable account credential you manage. It calls the API, and its scopes are permissions — they decide what the key may do, never what the key is. An API key can also enroll an agent interactively: the platform emails a one-time code to your account and the agent returns it. With the qurl:agent scope, the key can mint enrollment tokens.

02 

### Enrollment token — `kind: enrollment_token`

A single-use credential that enrolls exactly one target — an agent or a connector. It is the only headless enrollment path: no mailbox is involved, because the token was minted for one enrollment and is its own proof. It can be limited to one resource claim, so a token minted for the connector my-app can enroll nothing else.

03 

### Device credential — `kind: device`

When an agent or connector finishes enrolling, the platform mints this and the machine stores it. From that point on it is that machine’s identity: restarts reuse it, and no enrollment token or emailed code is needed again. It appears in credential listings so you can see and revoke it; it can never be created through the API.

### API key

**Lifetime:** Durable — no expiry  
**Proven by:** Proof of account ownership (console sign-in, or an emailed one-time code)  
**Created by:** You, in the console

### Enrollment token

**Lifetime:** One-shot, 24 hours maximum  
**Proven by:** The token itself  
**Created by:** You, from the console or an API key holding qurl:agent

### Device credential

**Lifetime:** Durable — the machine keeps it  
**Proven by:** Issued by the platform at the end of a successful enrollment  
**Created by:** The platform, never you

The rule behind the model

## Durable and general → the platform emails a code. Short-lived and purpose-built → the credential is its own proof.

An API key is durable and can do many things, so creating one — or enrolling a machine with one — has to prove the account really wants it. That proof is a one-time code the platform emails to the account.

An enrollment token is the opposite: it exists for one enrollment, expires within a day, and can be pinned to a single resource. There is nothing left to prove, so no code is emailed and no mailbox has to exist. That is why it is the only credential that works on a machine nobody can read mail on.

Creating credentials

## One endpoint, and `kind` picks what you get.

Both customer-creatable credentials come from `POST /v1/api-keys`. The plaintext secret is returned once, in the create response, and never again — store it before you close the terminal.

### An API key

Create API keys from the console, where your sign-in is the proof of account ownership. An existing API key cannot create another API key — that restriction is what stops a leaked key from quietly minting itself a wider one.

`scopes` is required here and lists exactly what the key may do.

POST /v1/api-keys

```
curl -X POST https://api.layerv.ai/v1/api-keys \
  -H "Authorization: Bearer $LAYERV_CONSOLE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "api_key",
    "name": "Production integration",
    "scopes": ["qurl:read", "qurl:write"]
  }'
```

201 Created

```
{
  "data": {
    "key_id": "key_abc123def456",
    "api_key": "lv_live_a3x9Kp2mN8qR5sT7wY0zBcDfGhJkLmNp",
    "key_prefix": "lv_live_a3x9",
    "kind": "api_key",
    "name": "Production integration",
    "scopes": ["qurl:read", "qurl:write"],
    "status": "active",
    "created_at": "2026-08-04T10:30:00Z"
  },
  "meta": { "request_id": "req_abc123" }
}
```

### An enrollment token for a connector

Set `target: connector` and bind the token to the connector id with one claim. A bound token can enroll that connector and nothing else, so it is safe to hand to whoever runs the install.

An API key can mint this if it holds `qurl:agent`. The connector id is 3 to 64 characters: lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit.

POST /v1/api-keys

```
curl -X POST https://api.layerv.ai/v1/api-keys \
  -H "Authorization: Bearer $QURL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "enrollment_token",
    "name": "prod-dashboard connector enrollment",
    "target": "connector",
    "claims": [{ "type": "connector", "id": "prod-dashboard" }],
    "expires_in": "2h"
  }'
```

201 Created

```
{
  "data": {
    "key_id": "key_def456abc123",
    "api_key": "lv_live_b7k2...",
    "key_prefix": "lv_live_b7k2",
    "kind": "enrollment_token",
    "target": "connector",
    "claims": [{ "type": "connector", "id": "prod-dashboard" }],
    "name": "prod-dashboard connector enrollment",
    "scopes": ["qurl:agent", "qurl:write"],
    "status": "active",
    "created_at": "2026-08-04T10:30:00Z",
    "expires_at": "2026-08-04T12:30:00Z"
  },
  "meta": { "request_id": "req_def456" }
}
```

### An enrollment token for an agent

Set `target: agent` and leave `claims` out for a token that can enroll any one agent. Add a connector claim if you want the agent restricted to a single connector.

`expires_in` is optional and defaults to 24 hours, which is also the ceiling. Mint the token close to when it will be used.

POST /v1/api-keys

```
curl -X POST https://api.layerv.ai/v1/api-keys \
  -H "Authorization: Bearer $QURL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "enrollment_token",
    "name": "headless agent enrollment",
    "target": "agent"
  }'
```

### Things the endpoint will not do

- It will not accept `scopes` on an enrollment token. Token scopes are server-assigned from `target`: an agent token gets `qurl:agent`, a connector token gets `qurl:agent` and `qurl:write`.
- It will not accept `target`, `claims`, or `expires_in` on an API key. API keys are durable and general by definition.
- It will not create a device credential. Those are minted by the platform when an enrollment completes.
- It will not let an enrollment token mint anything. A one-shot credential cannot create credentials.
- It will not accept more than one claim. One claim is the supported shape today.

### Seeing what you have

`GET /v1/api-keys` lists every credential on the account, including the device credentials the platform minted for your machines. The plaintext secret is never in a list response.

Names and scopes are editable on an API key. An enrollment token’s scopes are server-assigned and a device credential cannot be edited at all; a credential’s `kind` never changes.

GET /v1/api-keys

```
curl https://api.layerv.ai/v1/api-keys \
  -H "Authorization: Bearer $LAYERV_CONSOLE_TOKEN"
```

200 OK

```
{
  "data": [
    {
      "key_id": "key_abc123def456",
      "key_prefix": "lv_live_a3x9",
      "kind": "api_key",
      "name": "Production integration",
      "scopes": ["qurl:read", "qurl:write"],
      "status": "active"
    },
    {
      "key_id": "key_ghi789jkl012",
      "key_prefix": "lv_live_c4m8",
      "kind": "device",
      "name": "prod-dashboard (host-7)",
      "scopes": ["qurl:agent", "qurl:write"],
      "status": "active"
    }
  ],
  "meta": { "request_id": "req_ghi789", "page_size": 2, "has_more": false }
}
```

Scopes

## Scopes are permissions. They are not a credential type.

A scope narrows what an API key may do. It never changes what the key is: an API key with `qurl:agent` is an ordinary durable API key that can also mint enrollment tokens. For a credential that is narrow in *time* and pinned to one resource, reach for an enrollment token, not a scope.

### `qurl:read`

Read qURLs, resources, and quota.

### `qurl:write`

Create, update, and delete qURLs and resources. Also covers webhook management.

### `qurl:resolve`

Redeem access tokens headlessly and share resources by CRID.

### `qurl:agent`

Mint enrollment tokens. An API key without this scope cannot create an enrollment token for either target.

### Who may mint what

- Console callers may create both an API key and an enrollment token.
- An API key holding `qurl:agent` may create enrollment tokens for either target — and nothing else.
- An API key without `qurl:agent` may not create any credential.
- An enrollment token may not create any credential.

Which one do I want?

## Start from the job, not the credential.

### I want to call the API from my backend.

An API key. Create it in the console, give it only the scopes that backend needs, and store it in your secret manager.

### I am starting a service or agent and something can read a mailbox.

An API key plus the emailed code. This is the default path and needs nothing minted in advance — see the Go SDK agent guide.

### I am starting a service or agent and nothing can read a mailbox.

An enrollment token with target=agent. Mint it just before the run and pass it to the runtime; it expires within 24 hours whether it is used or not.

### I am installing a connector next to a private service.

An enrollment token with target=connector, bound to that connector id with a claim. The connector consumes it on first start.

### My machine already enrolled and is restarting.

Nothing. It holds a device credential. Do not mint a second enrollment token for a machine that already enrolled.

### I want one credential for a CI job that only reads.

An API key scoped to qurl:read. Scopes are how you narrow an API key; there is no smaller durable credential.

### Then follow the guide for that machine

The credential is only the first step. Both onboarding guides pick up from the credential you just chose.

[Connect an agent with the Go SDK](https://layerv.ai/docs/agents/)

The create request

## The API is kind-first. `kind` decides the rest.

`POST /v1/api-keys` takes `kind` on every create, and validates every other field against it. A create request without \`kind\` is rejected, and so is \`key\_type\` — it is not a field this API accepts. If a client is sending it, this is the shape to send instead.

### `kind`

Required on every create. \`api\_key\` or \`enrollment\_token\`. It is the discriminator: every other field is accepted or rejected based on it.

### `name`

Required. A human-readable label, up to 100 characters.

### `scopes`

Required for \`kind: api\_key\`, and accepted only there. Enrollment-token scopes are server-assigned from \`target\`; sending the field on a token returns 400 \`invalid\_input\`.

### `target`

Enrollment tokens only. \`connector\` or \`agent\` — what this token enrolls. Omit it and the API derives it from \`claims\`: a connector claim means \`connector\`, otherwise \`agent\`.

### `claims`

Enrollment tokens only. Binds the token to one resource: \`\[{ "type": "connector", "id": "prod-dashboard" }\]\`. \`target: connector\` requires exactly one connector claim; \`target: agent\` takes zero or one.

### `expires_in`

Enrollment tokens only. Defaults to 24h and cannot exceed it. API keys are durable and reject the field.

### A create request in full

This one mints a connector enrollment token bound to `prod-dashboard`. The response returns the one-time secret in the standard envelope.

Request body

```
{
  "kind": "enrollment_token",
  "name": "prod-dashboard connector enrollment",
  "target": "connector",
  "claims": [{ "type": "connector", "id": "prod-dashboard" }],
  "expires_in": "2h"
}
```

### What responses carry

- `kind` is on every credential in create, get, and list responses. It is the field to switch on.
- `target` and `claims` appear on enrollment tokens. `claims` is present only when the token is bound.
- Responses carry no `key_type`. A client reading that field gets `undefined`.
- Treat `kind` as an open set. New kinds may appear as the model grows, so parse unknown values without failing — switch on the kinds you handle and ignore the rest rather than rejecting the response.

Errors

## What a rejected create is telling you.

Validation errors return 400 with a code in `error.code`. A 403 means the caller was not allowed to mint that kind — most often an API key without `qurl:agent`, or an API key trying to create an API key.

### `invalid_input`

The request mixes fields that do not belong to the kind you asked for — scopes on an enrollment token, target or claims on an API key, expires\_in on an API key, more than one claim, or a claim id that does not match the id format.

### `invalid_duration`

expires\_in is malformed or longer than 24 hours. The format is a number followed by s, m, h, d, or w.

### `bootstrap_key_consumed`

Returned with 409\. The enrollment token was already consumed by another enrollment, so it is terminal. Mint a new one; never retry the same token concurrently.

### `api_key_limit`

The account reached its plan credential limit. Check the quota response for the current allowance.

### `email_verification_required`

The account email is not verified yet, so it cannot be proven for a durable credential.

Next

## Pick a credential, then enroll something.

The full request and response schemas, including every field and status code, live in the API reference.

[Open the API reference](https://layerv.ai/docs/) [Connect an agent](https://layerv.ai/docs/agents/)



---

*This markdown version is auto-generated from [https://layerv.ai/docs/credentials/](https://layerv.ai/docs/credentials/) for AI agents. Curated agent resources: [llms.txt](https://layerv.ai/llms.txt). For the full interactive experience, visit the HTML version.*

