Device and offline access
Use this API family to create an offline-access grant under an existing principal, collect the approved token package, refresh its bundle, and manage the resulting grant records.
This is not the public OAuth device-authorization start for a relying party.
For that standards-facing flow, call
POST /device_authorization
on M7 SSO. The api.user routes below require an existing bearer token.
Authentication
Send a consumer-user bearer token:
Authorization: Bearer <ACCESS_TOKEN>
Issue, pickup, refresh, acknowledgement, search, and view also support the restricted personal owner-proxy machine profile described in the authorization guide. Organization application tokens are not accepted by this family, and revoke always requires a human consumer-user session.
Issue, pickup, refresh, and acknowledgement calls operate in the current principal context. Use the same principal that created the grant when polling, refreshing, acknowledging, viewing, or revoking its record.
Endpoint summary
| Route | Required fields | Notes |
|---|---|---|
/oauth/device/issue |
client_id |
Starts an offline-access grant under the current principal. |
/oauth/device/pickup |
id; client_secret for a confidential client |
Polls the grant after approval. |
/oauth/device/refresh/bundle |
Refresh bundle | Refreshes or rotates a saved token bundle. |
/oauth/device/acknowledge/refresh/bundle |
Pending bundle |
Activates an ACK-mode refresh package. |
/oauth/device/revoke |
id; password when revoking a live server-backed refresh session |
Optional force for an eligible record that cannot be invalidated upstream. Machine credentials cannot revoke through this route. |
/oauth/device/view |
id |
Returns a grant record. Client-credentials records use the corresponding issued-record view. |
/oauth/device/search |
None | Returns both current-user and accessible-application grant sets. Request filters are not applied. |
/oauth/device/search/app |
None | Grant records for an application-management view. |
/oauth/device/search/user |
None | Grant records for the current user; supports pagination. |
Start an offline-access grant
Request fields
| Field | Required | Notes |
|---|---|---|
client_id |
Yes | Public OAuth client ID. |
client_secret |
Conditional | Required for confidential clients. |
scope |
No | Requested scopes; prefer a space-delimited string. |
aud |
No | Requested audience, as a string or array of strings. |
access_expires |
No | Requested access-token lifetime in seconds. |
refresh_expires |
No | Requested refresh-token lifetime in seconds. |
claims |
No | Legacy claim object used when split claim fields are absent. |
access_claims |
No | Requested access-token claim object. |
refresh_claims |
No | Requested refresh-token claim object. |
fingerprint |
No | Optional binding value. |
label |
No | Display label stored with the offline-access record. |
display_type |
No | iframe (default) or popup. Use the returned verification URLs unchanged. |
Requested token lifetime and claim values are subject to the OAuth application's configured policy.
Request example
curl -sS https://api.user.m7.org/api/v2/oauth/device/issue \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"client_id": "PUBLIC_CLIENT_ID",
"scope": "openid profile offline_access",
"aud": "https://api.example.m7.org",
"access_expires": 3600,
"refresh_expires": 2592000,
"access_claims": {
"installation": "cli"
},
"refresh_claims": {
"grant_kind": "offline_access"
},
"label": "Example CLI",
"display_type": "popup"
}'
Success response
The result contains the information needed to render the user step and control
polling. It returns display_type in both verification URLs.
{
"status": 1,
"data": {
"id": "OFFLINE_ACCESS_ID",
"status": "pending",
"client_id": "PUBLIC_CLIENT_ID",
"requested_scope": "openid profile offline_access",
"requested_aud": ["https://api.example.m7.org"],
"user_code": "ABCD-EFGH",
"verification_uri": "https://sso.user.m7.org/device_login?display_type=popup",
"verification_uri_complete": "https://sso.user.m7.org/device_login?user_code=ABCD-EFGH&display_type=popup",
"expires_in": 600,
"interval": 5
}
}
Poll for pickup
Call /oauth/device/pickup after the user completes the returned verification
URL. Poll no more frequently than interval and stop after expires_in.
curl -sS https://api.user.m7.org/api/v2/oauth/device/pickup \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"id": "OFFLINE_ACCESS_ID"
}'
Send client_secret again when the application is confidential.
A grant that is still awaiting approval is a successful API response with a pending grant state, not necessarily an HTTP error:
{
"status": 1,
"data": {
"id": "OFFLINE_ACCESS_ID",
"status": "pending",
"interval": 5,
"comment": "authorization pending"
}
}
After approval, data contains the token package and grant metadata. A revoked,
closed, or abandoned grant returns a grant state and invalid_grant details;
do not continue polling it.
{
"status": 1,
"data": {
"id": "OFFLINE_ACCESS_ID",
"status": "success",
"client_id": "PUBLIC_CLIENT_ID",
"token_type": "bearer",
"access_token": "ACCESS_TOKEN",
"expires_in": 3600,
"refresh_token": "REFRESH_TOKEN",
"refresh_expires_in": 2592000,
"binding_chain": "BINDING_CHAIN",
"binding_link": "BINDING_LINK",
"scope": "openid profile offline_access",
"aud": ["https://api.example.m7.org"]
}
}
Refresh a token bundle
/oauth/device/refresh/bundle refreshes a previously issued bundle. Send
bundle as an object or JSON string. It may contain refresh_token,
binding_chain, binding_link, and client_id. Top-level refresh_token,
binding_chain, and binding_link override values in bundle.
| Field | Required | Notes |
|---|---|---|
bundle |
Yes, unless all bundle values are sent at top level | Current bundle object or JSON string. |
client_id |
Conditional | Required unless it is already in bundle.client_id. |
client_secret |
Conditional | Required for confidential clients. |
fingerprint |
No | 64-character hexadecimal string. |
new_fingerprint |
No | 64-character hexadecimal string; defaults to fingerprint when omitted. |
refresh_mode |
No | Omit for the SSO default, strict_rotation. Allowed values: static, strict_rotation, grace_rotation, ack, ack_supersede_pending. |
Refresh example
curl -sS https://api.user.m7.org/api/v2/oauth/device/refresh/bundle \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"client_id": "PUBLIC_CLIENT_ID",
"client_secret": "CLIENT_SECRET",
"refresh_mode": "ack",
"bundle": {
"client_id": "PUBLIC_CLIENT_ID",
"refresh_token": "REFRESH_TOKEN",
"binding_chain": "BINDING_CHAIN",
"binding_link": "BINDING_LINK"
}
}'
A successful normal refresh returns data.success: 1, a replacement bundle,
and token_state: "active". Replace stored credentials only when the returned
token state is active.
An ACK refresh returns a successful pending package:
{
"status": 1,
"data": {
"success": 1,
"status": "pending",
"token_state": "pending",
"bundle": {
"client_id": "PUBLIC_CLIENT_ID",
"refresh_token": "REFRESH_TOKEN",
"token_state": "pending",
"activation": {
"id": "ACTIVATION_ID",
"secret": "ACTIVATION_SECRET"
},
"predecessor_bundle": {
"client_id": "PUBLIC_CLIENT_ID",
"refresh_token": "PREVIOUS_REFRESH_TOKEN"
}
}
}
}
Keep this exact pending bundle until it is acknowledged. Do not replace the currently active stored bundle with it yet.
Acknowledge a pending refresh
Send the pending bundle from an ACK refresh to
/oauth/device/acknowledge/refresh/bundle.
curl -sS https://api.user.m7.org/api/v2/oauth/device/acknowledge/refresh/bundle \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"bundle": {
"client_id": "PUBLIC_CLIENT_ID",
"token_state": "pending",
"activation": {
"id": "ACTIVATION_ID",
"secret": "ACTIVATION_SECRET"
}
}
}'
On success, the response changes token_state to active and removes
activation and predecessor_bundle. If acknowledgement fails, retain the
original pending package so it can be retried.
Search, view, and revoke
| Route | Request fields | Notes |
|---|---|---|
/oauth/device/search |
None | Returns both data.user and data.app record sets. The user set is constrained to the current user's principal with principal_type: "user"; the app set is constrained to OAuth applications owned by that user. |
/oauth/device/search/app |
None | Returns device-grant records for OAuth applications owned by the current user. |
/oauth/device/search/user |
Shared pagination only | Returns the current user's device-grant records. The handler does not accept caller-selected id, status, client_id, or principal filters. |
/oauth/device/view |
id |
Returns one record. |
/oauth/device/revoke |
id; conditional password; optional force |
Closes or revokes the selected grant for an eligible user principal. |
For a live record with a refresh-token session ID, also send password (or its
pass alias) so the service can revoke the upstream session. If the record has
refresh material but no revocable session ID, the request fails unless
force: true; forced handling marks the local record abandoned without
claiming upstream invalidation.
Use the shared pagination contract for list results.