Auth
Sign-in and token issuance. Humans use passkeys (WebAuthn). The CLI uses the OAuth 2.0 device flow
(RFC 8628) and receives a one-hour access token plus a refresh token. A development-only login exists
when the server runs with GUV_DEV_LOGIN=true.
| Operation | Method and path |
|---|---|
| Sign in without a passkey (development only) | POST /v1/auth/dev-login |
| Send a sign-up verification code by email | POST /v1/auth/email-verification |
| Start registering a passkey (sign-up, or add a passkey when signed in) | POST /v1/auth/passkeys/register/begin |
| Finish registering a passkey | POST /v1/auth/passkeys/register/finish |
| Start signing in with a passkey | POST /v1/auth/passkeys/login/begin |
| Finish signing in with a passkey | POST /v1/auth/passkeys/login/finish |
| Start a passkey assertion for a sensitive action | POST /v1/auth/step-up/begin |
| Start the CLI device flow (RFC 8628) | POST /v1/auth/device/code |
| Show which device is asking to sign in | GET /v1/auth/device |
| Approve or deny a CLI sign-in (signed-in user) | POST /v1/auth/device/decision |
| Exchange a device code, authorisation code or refresh token for an access token | POST /v1/auth/token |
| Sign out the current session | POST /v1/auth/logout |
Sign in without a passkey (development only)
Section titled “Sign in without a passkey (development only)”POST /v1/auth/dev-loginExists only when the server runs with GUV_DEV_LOGIN=true; otherwise returns 404 dev_login_disabled. Creates the user (and a personal workspace) if needed and sets the session cookie.
- Operation:
devLogin - Scopes: none (public)
- CLI:
guv login --dev - Idempotency-Key: optional
Request body (application/json, required): DevLoginRequest
| Field | Type | Description |
|---|---|---|
email * |
string (email) | |
name |
string | |
platform_admin |
boolean | Make the user a platform admin (development only). Default false. |
create_workspace |
boolean | Create a personal workspace if the user has none. Default true. |
Responses
| Status | Meaning | Body |
|---|---|---|
200 OK |
OK. | LoginResponse |
Errors: 400, 401, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/dev-login \ -H "Authorization: Bearer $GUV_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "dev@guv.local", "name": "Dev User", "platform_admin": false, "create_workspace": true}'Example response
{ "session": { "id": "ses_01jbx2m9k7q4r8t5v6w3y0z1pr", "kind": "browser", "auth_method": "passkey", "user_agent": "string", "ip": "string", "current": false, "created_at": "2026-10-22T13:05:00Z", "last_seen_at": "2026-10-22T13:05:00Z", "expires_at": "2026-10-22T13:05:00Z" }, "access_token": "string", "csrf_token": "string", "user": { "id": "usr_01jbx2m9k7q4r8t5v6w3y0z1us", "email": "stewart@primecarers.co.uk", "name": "Stewart Webb", "avatar_url": null, "github_login": "stewartwebb", "has_passkey": true, "is_platform_admin": true, "default_workspace_id": "ws_01jbx2m9k7q4r8t5v6w3y0z1ws", "created_at": "2026-10-22T09:00:00Z" }, "new_user": false}Send a sign-up verification code by email
Section titled “Send a sign-up verification code by email”POST /v1/auth/email-verification- Operation:
startEmailVerification - Scopes: none (public)
- Idempotency-Key: optional
Request body (application/json, required): EmailVerificationRequest
| Field | Type | Description |
|---|---|---|
email * |
string (email) |
Responses
| Status | Meaning | Body |
|---|---|---|
202 Accepted |
Accepted. | EmailVerificationStarted |
Errors: 400, 401, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/email-verification \ -H "Authorization: Bearer $GUV_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "ada@example.co.uk"}'Example response
{ "verification_id": "string", "expires_at": "2026-10-22T13:05:00Z"}Start registering a passkey (sign-up, or add a passkey when signed in)
Section titled “Start registering a passkey (sign-up, or add a passkey when signed in)”POST /v1/auth/passkeys/register/begin- Operation:
beginPasskeyRegistration - Scopes: any signed-in principal
- Idempotency-Key: optional
Request body (application/json, required): PasskeyRegisterBeginRequest
| Field | Type | Description |
|---|---|---|
name |
string | Label for the passkey. |
email |
string (email) | |
display_name |
string | |
verification_id |
string | |
code |
string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 OK |
OK. | PasskeyChallenge |
Errors: 400, 401, 403, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/passkeys/register/begin \ -H "Authorization: Bearer $GUV_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "MacBook Touch ID", "email": "ada@example.co.uk", "display_name": "string", "verification_id": "string", "code": "string"}'Example response
{ "challenge_id": "string", "expires_at": "2026-10-22T13:05:00Z", "public_key_creation": { "challenge": "string", "rp": { "id": "guv.dev", "name": "Guv" }, "user": { "id": "string", "name": "string", "displayName": "string" }, "pubKeyCredParams": [ {} ], "timeout": 1, "authenticatorSelection": {}, "attestation": "string", "excludeCredentials": [ {} ] }, "public_key_request": { "challenge": "string", "rpId": "string", "timeout": 1, "userVerification": "required", "allowCredentials": [ {} ] }}Finish registering a passkey
Section titled “Finish registering a passkey”POST /v1/auth/passkeys/register/finishSigns the new user in when this completes a sign-up.
- Operation:
finishPasskeyRegistration - Scopes: any signed-in principal
- Idempotency-Key: optional
Request body (application/json, required): PasskeyFinishRequest
| Field | Type | Description |
|---|---|---|
challenge_id * |
string | |
credential * |
WebAuthnCredential | |
name |
string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 OK |
OK. | LoginResponse |
Errors: 400, 401, 403, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/passkeys/register/finish \ -H "Authorization: Bearer $GUV_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "challenge_id": "string", "credential": { "id": "string", "rawId": "string", "type": "public-key", "response": {}, "authenticatorAttachment": "string", "clientExtensionResults": {} }, "name": "string"}'Example response
{ "session": { "id": "ses_01jbx2m9k7q4r8t5v6w3y0z1pr", "kind": "browser", "auth_method": "passkey", "user_agent": "string", "ip": "string", "current": false, "created_at": "2026-10-22T13:05:00Z", "last_seen_at": "2026-10-22T13:05:00Z", "expires_at": "2026-10-22T13:05:00Z" }, "access_token": "string", "csrf_token": "string", "user": { "id": "usr_01jbx2m9k7q4r8t5v6w3y0z1us", "email": "stewart@primecarers.co.uk", "name": "Stewart Webb", "avatar_url": null, "github_login": "stewartwebb", "has_passkey": true, "is_platform_admin": true, "default_workspace_id": "ws_01jbx2m9k7q4r8t5v6w3y0z1ws", "created_at": "2026-10-22T09:00:00Z" }, "new_user": false}Start signing in with a passkey
Section titled “Start signing in with a passkey”POST /v1/auth/passkeys/login/begin- Operation:
beginPasskeyLogin - Scopes: none (public)
- Idempotency-Key: optional
Request body (application/json, required): PasskeyLoginBeginRequest
| Field | Type | Description |
|---|---|---|
email |
string (email) | Optional; omit for discoverable credentials (passkey autofill). |
Responses
| Status | Meaning | Body |
|---|---|---|
200 OK |
OK. | PasskeyChallenge |
Errors: 400, 401, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/passkeys/login/begin \ -H "Authorization: Bearer $GUV_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "ada@example.co.uk"}'Example response
{ "challenge_id": "string", "expires_at": "2026-10-22T13:05:00Z", "public_key_creation": { "challenge": "string", "rp": { "id": "guv.dev", "name": "Guv" }, "user": { "id": "string", "name": "string", "displayName": "string" }, "pubKeyCredParams": [ {} ], "timeout": 1, "authenticatorSelection": {}, "attestation": "string", "excludeCredentials": [ {} ] }, "public_key_request": { "challenge": "string", "rpId": "string", "timeout": 1, "userVerification": "required", "allowCredentials": [ {} ] }}Finish signing in with a passkey
Section titled “Finish signing in with a passkey”POST /v1/auth/passkeys/login/finish- Operation:
finishPasskeyLogin - Scopes: none (public)
- Idempotency-Key: optional
Request body (application/json, required): PasskeyFinishRequest
| Field | Type | Description |
|---|---|---|
challenge_id * |
string | |
credential * |
WebAuthnCredential | |
name |
string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 OK |
OK. | LoginResponse |
Errors: 400, 401, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/passkeys/login/finish \ -H "Authorization: Bearer $GUV_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "challenge_id": "string", "credential": { "id": "string", "rawId": "string", "type": "public-key", "response": {}, "authenticatorAttachment": "string", "clientExtensionResults": {} }, "name": "string"}'Example response
{ "session": { "id": "ses_01jbx2m9k7q4r8t5v6w3y0z1pr", "kind": "browser", "auth_method": "passkey", "user_agent": "string", "ip": "string", "current": false, "created_at": "2026-10-22T13:05:00Z", "last_seen_at": "2026-10-22T13:05:00Z", "expires_at": "2026-10-22T13:05:00Z" }, "access_token": "string", "csrf_token": "string", "user": { "id": "usr_01jbx2m9k7q4r8t5v6w3y0z1us", "email": "stewart@primecarers.co.uk", "name": "Stewart Webb", "avatar_url": null, "github_login": "stewartwebb", "has_passkey": true, "is_platform_admin": true, "default_workspace_id": "ws_01jbx2m9k7q4r8t5v6w3y0z1ws", "created_at": "2026-10-22T09:00:00Z" }, "new_user": false}Start a passkey assertion for a sensitive action
Section titled “Start a passkey assertion for a sensitive action”POST /v1/auth/step-up/beginUsed before approving an agent request, revealing credentials, deleting resources, raising a spend cap or changing the kill switch. The resulting PasskeyAssertion is sent with that action.
- Operation:
beginStepUp - Scopes: any signed-in principal
- Idempotency-Key: optional
Request body (application/json, required): StepUpBeginRequest
| Field | Type | Description |
|---|---|---|
purpose * |
approval, kill_switch, reveal_credentials, delete_resource, raise_spend_cap |
|
subject_id |
string | The approval, database or other resource the assertion is bound to. |
Responses
| Status | Meaning | Body |
|---|---|---|
200 OK |
OK. | PasskeyChallenge |
Errors: 400, 401, 403, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/step-up/begin \ -H "Authorization: Bearer $GUV_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "purpose": "approval", "subject_id": "apr_01jbx2m9k7q4r8t5v6w3y0z1ap"}'Example response
{ "challenge_id": "string", "expires_at": "2026-10-22T13:05:00Z", "public_key_creation": { "challenge": "string", "rp": { "id": "guv.dev", "name": "Guv" }, "user": { "id": "string", "name": "string", "displayName": "string" }, "pubKeyCredParams": [ {} ], "timeout": 1, "authenticatorSelection": {}, "attestation": "string", "excludeCredentials": [ {} ] }, "public_key_request": { "challenge": "string", "rpId": "string", "timeout": 1, "userVerification": "required", "allowCredentials": [ {} ] }}Start the CLI device flow (RFC 8628)
Section titled “Start the CLI device flow (RFC 8628)”POST /v1/auth/device/code- Operation:
requestDeviceCode - Scopes: none (public)
- CLI:
guv login - Idempotency-Key: optional
Request body (application/json, required): DeviceCodeRequest
| Field | Type | Description |
|---|---|---|
client_id * |
string | |
scope |
string | Space-separated scopes. Defaults to the user’s full role. |
device_name |
string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 OK |
OK. | DeviceCodeResponse |
Errors: 400, 401, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/device/code \ -H "Authorization: Bearer $GUV_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "client_id": "guv-cli", "scope": "string", "device_name": "stewart-mbp"}'Example response
{ "device_code": "string", "user_code": "BDWP-HQPK", "verification_uri": "https://guv.dev/device", "verification_uri_complete": "https://guv.dev/device?code=BDWP-HQPK", "expires_in": 600, "interval": 5}Show which device is asking to sign in
Section titled “Show which device is asking to sign in”GET /v1/auth/device- Operation:
lookupDeviceCode - Scopes: any signed-in principal
Parameters
| Name | In | Type | Description |
|---|---|---|---|
user_code * |
query | string | Code shown in the terminal. |
Responses
| Status | Meaning | Body |
|---|---|---|
200 OK |
OK. | DeviceLookup |
Errors: 400, 401, 403, 429, 500, as problem+json with a stable code.
Example
curl https://api.guv.dev/v1/auth/device \ -H "Authorization: Bearer $GUV_TOKEN"Example response
{ "user_code": "string", "client_id": "string", "device_name": "string", "ip": "string", "requested_at": "2026-10-22T13:05:00Z", "scopes": [ "deploy:staging" ]}Approve or deny a CLI sign-in (signed-in user)
Section titled “Approve or deny a CLI sign-in (signed-in user)”POST /v1/auth/device/decision- Operation:
decideDeviceCode - Scopes: any signed-in principal
- Idempotency-Key: required
Request body (application/json, required): DeviceDecisionRequest
| Field | Type | Description |
|---|---|---|
user_code * |
string | |
decision * |
approve, deny |
Responses
| Status | Meaning | Body |
|---|---|---|
204 No Content |
Done. No body. | none |
Errors: 400, 401, 403, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/device/decision \ -H "Authorization: Bearer $GUV_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "user_code": "string", "decision": "approve"}'Exchange a device code, authorisation code or refresh token for an access token
Section titled “Exchange a device code, authorisation code or refresh token for an access token”POST /v1/auth/tokenErrors use problem+json with the OAuth error field set (authorization_pending, slow_down, expired_token, access_denied, invalid_grant).
- Operation:
issueToken - Scopes: none (public)
Request body (application/json, required): TokenRequest
| Field | Type | Description |
|---|---|---|
grant_type * |
urn:ietf:params:oauth:grant-type:device_code, refresh_token, authorization_code |
|
client_id * |
string | |
device_code |
string | |
refresh_token |
string | |
code |
string | |
code_verifier |
string | |
redirect_uri |
string |
Responses
| Status | Meaning | Body |
|---|---|---|
200 OK |
OK. | TokenResponse |
Errors: 400, 401, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/token \ -H "Authorization: Bearer $GUV_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "client_id": "string", "device_code": "string", "refresh_token": "string", "code": "string", "code_verifier": "string", "redirect_uri": "string"}'Example response
{ "access_token": "string", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "string", "scope": "string"}Sign out the current session
Section titled “Sign out the current session”POST /v1/auth/logout- Operation:
logout - Scopes: any signed-in principal
- CLI:
guv logout - Idempotency-Key: optional
Responses
| Status | Meaning | Body |
|---|---|---|
204 No Content |
Done. No body. | none |
Errors: 400, 401, 403, 409, 422, 429, 500, as problem+json with a stable code.
Example
curl -X POST https://api.guv.dev/v1/auth/logout \ -H "Authorization: Bearer $GUV_TOKEN"