API reference
Device authorization API
The RFC 8628 device grant that vy login uses to receive an API key.
- Request a device code:
POST /v1/auth/device/code - Poll for the API key:
POST /v1/auth/device/token - Look up a device request:
GET /v1/auth/device - Approve a device:
POST /v1/auth/device/approve - Deny a device:
POST /v1/auth/device/deny
Request a device code
POST /v1/auth/device/code
- Authentication: none on this route.
- Who: No authentication.
Request body (application/json, DeviceCodeRequest)
| Field | Type | Required | Description |
|---|---|---|---|
client_name | string | null | No | Shown to the approving user, such as vy on build-host; 1–64 characters. Defaults to vy CLI. |
scope | null | ApiKeyPresetDto | No | The preset the CLI asks for; the approving user may choose another. Defaults to create_and_use. |
Responses
| Status | Description | Body |
|---|---|---|
200 | Show user_code and verification_uri, then poll /v1/auth/device/token every interval seconds | DeviceCodeResponse |
422 | Invalid request (invalid_request); field_errors names the fields. | ProblemDetails |
429 | Too many requests; wait for Retry-After. | ProblemDetails |
curl -X POST "$VMYARD_API_URL/v1/auth/device/code" \
-H "Content-Type: application/json" \
-d '{
"client_name": "vy on build-host",
"scope": "use_existing"
}'{
"device_code": "string",
"expires_in": 0,
"interval": 0,
"user_code": "BCDF-GHJK",
"verification_uri": "https://console.example.com/device",
"verification_uri_complete": "https://console.example.com/device?user_code=BCDF-GHJK"
}Poll for the API key
POST /v1/auth/device/token
Polls for the API key. Errors use the RFC 8628 shape, not problem details.
- Authentication: none on this route.
- Who: No authentication.
Request body (application/json, DeviceTokenRequest)
| Field | Type | Required | Description |
|---|---|---|---|
device_code | string | Yes | |
grant_type | string | Yes | urn:ietf:params:oauth:grant-type:device_code |
Responses
| Status | Description | Body |
|---|---|---|
200 | Approved: the new API key, returned exactly once | DeviceTokenResponse |
400 | authorization_pending, slow_down (add 5 s to the interval), access_denied, expired_token, invalid_grant, invalid_request, or unsupported_grant_type | DeviceTokenError |
curl -X POST "$VMYARD_API_URL/v1/auth/device/token" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": "<device_code>"
}'{
"access_token": "vmy_…",
"api_key": {
"created_at": "2026-10-12T05:51:01Z",
"created_by": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
"expires_at": "2026-10-12T05:51:01Z",
"id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
"last_four": "x9Q2",
"last_used_at": "2026-10-12T05:51:01Z",
"name": "ci-runner",
"organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
"prefix": "vmy_AbCdEfGh",
"preset": "read_only",
"revoked_at": "2026-10-12T05:51:01Z"
},
"expires_in": 0,
"scope": "read_only",
"token_type": "Bearer"
}Look up a device request
GET /v1/auth/device
Shows a pending request so the user can check the device name and requested scope before approving.
- API key: not accepted (
403 session_required). - Browser session: the
vmyard_sessioncookie. - Who: Any signed-in user.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
user_code | query | string | Yes | The code the user typed; case, spaces, and dashes are ignored. |
Responses
| Status | Description | Body |
|---|---|---|
200 | — | DeviceAuthorizationDto |
401 | Not signed in, or the API key is malformed, expired, or revoked (unauthenticated, invalid_api_key). | ProblemDetails |
403 | session_required for API keys | ProblemDetails |
404 | Unknown, expired, or already decided code | ProblemDetails |
429 | Too many requests; wait for Retry-After. | ProblemDetails |
curl "$VMYARD_API_URL/v1/auth/device?user_code=BCDF-GHJK" \
-b "vmyard_session=$VMYARD_SESSION"{
"client_name": "vy on build-host",
"expires_at": "2026-10-12T05:51:01Z",
"requested_scope": "read_only",
"user_code": "BCDF-GHJK"
}Approve a device
POST /v1/auth/device/approve
Approves a pending device for the X-Organization-ID organization. The
key is minted for this user when the device next polls, so it is bound by
the user's role there like any other key.
- API key: not accepted (
403 session_required). - Browser session: the
vmyard_sessioncookie and thex-csrf-tokenheader. - Who: Any member of the chosen organization.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Organization-ID | header | string (uuid) | No | Organization the key will belong to. Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |
Request body (application/json, DeviceApproveRequest)
| Field | Type | Required | Description |
|---|---|---|---|
user_code | string | Yes | |
expires_in_days | integer | null | No | Key lifetime, 1–90 days; defaults to 90. 1–90. default 90. |
name | string | null | No | Key name, 1–64 characters; defaults to vy: <client_name>. |
preset | null | ApiKeyPresetDto | No | The key's preset; defaults to the requested scope. |
Responses
| Status | Description | Body |
|---|---|---|
200 | — | DeviceApproved |
401 | Not signed in, or the API key is malformed, expired, or revoked (unauthenticated, invalid_api_key). | ProblemDetails |
403 | CSRF failure or an API key (session_required) | ProblemDetails |
404 | Unknown, expired, or already decided code, or not a member of the organization | ProblemDetails |
422 | Invalid request (invalid_request); field_errors names the fields. | ProblemDetails |
429 | Too many requests; wait for Retry-After. | ProblemDetails |
curl -X POST "$VMYARD_API_URL/v1/auth/device/approve" \
-b "vmyard_session=$VMYARD_SESSION" \
-H "x-csrf-token: $VMYARD_CSRF" \
-H "X-Organization-ID: $VMYARD_ORGANIZATION_ID" \
-H "Content-Type: application/json" \
-d '{
"user_code": "BCDF-GHJK",
"preset": "use_existing",
"expires_in_days": 30,
"name": "vy: build-host"
}'{
"expires_in_days": 0,
"organization_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
"preset": "read_only",
"user_code": "BCDF-GHJK"
}Deny a device
POST /v1/auth/device/deny
- API key: not accepted (
403 session_required). - Browser session: the
vmyard_sessioncookie and thex-csrf-tokenheader. - Who: Any signed-in user.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Organization-ID | header | string (uuid) | No | Organization that records the audit event. Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |
Request body (application/json, DeviceDecisionRequest)
| Field | Type | Required | Description |
|---|---|---|---|
user_code | string | Yes |
Responses
| Status | Description | Body |
|---|---|---|
204 | Denied; the device's next poll gets access_denied | — |
401 | Not signed in, or the API key is malformed, expired, or revoked (unauthenticated, invalid_api_key). | ProblemDetails |
403 | Refused for this caller: role, key preset, CSRF token, or organization (forbidden, insufficient_scope, csrf_invalid, organization_mismatch, session_required). | ProblemDetails |
404 | Not found in this organization (resource_not_found). | ProblemDetails |
429 | Too many requests; wait for Retry-After. | ProblemDetails |
curl -X POST "$VMYARD_API_URL/v1/auth/device/deny" \
-b "vmyard_session=$VMYARD_SESSION" \
-H "x-csrf-token: $VMYARD_CSRF" \
-H "X-Organization-ID: $VMYARD_ORGANIZATION_ID" \
-H "Content-Type: application/json" \
-d '{
"user_code": "BCDF-GHJK"
}'