Device flow

The device flow connects an agent that cannot open a browser, such as a sandboxed cloud agent or a CI job. It follows RFC 8628. The agent asks the relay for a short code and shows it to the user. The user approves the request on the Mac, and the agent collects its tokens by polling. This page documents the three routes of the flow: the device authorization request, the activation page and the device grant of the token request. The step-by-step for a person setting this up is in Device flow. The examples use $RELAY as defined in Relay API.

Before you start#

  • The relay is paired with at least one Mac. See Pairing and devices.
  • The client is registered with the device grant. See POST /register. A client that only uses the device flow sends no redirect_uris.

Concepts#

The flow has four steps.

  1. The agent calls POST /device_authorization and receives a device_code, a user_code and an interval.
  2. The agent shows the user_code and the name of the Mac to the user.
  3. The user approves on the Mac: a banner with Approve and Deny, or Settings > Cloud > Connector approvals, or the relay's /activate page.
  4. The agent polls POST /token every interval seconds until it gets tokens or an error that ends the flow.
ValueWhat it is
device_codeA secret that begins hrv_. Only the agent holds it. It is how the agent polls.
user_codeEight consonants as XXXX-XXXX. The user matches it in Herald. It is not a secret.
Approval codeSix digits, shown only in Herald. The /activate page asks for it so that the agent, which knows the user_code, cannot approve itself.

The tokens that come out are the same as in the browser flow. An approved connector works until revoked, its tokens never expire and are never rotated, and expires_in is ten years. See Token lifetime. A repeated poll after approval returns tokens again.

Endpoints#

EndpointPurpose
POST /device_authorizationStart a device request and show a banner on the Mac.
GET /activateServe the page where a person enters the code and approves.
POST /tokenPoll with the device code.

POST /device_authorization#

Starts the device flow for an agent that has no usable browser. The reply holds a short user_code that the agent shows to the user, and a device_code that the agent keeps and polls with. At the same moment, Herald shows an approval banner on the Mac.

Request

The body is a form. A confidential client also sends its secret.

NameInTypeRequiredDescription
client_idbodystringrequiredThe id from POST /register.
scopebodystringoptionalnotify.
devicebodyintegeroptionalWhich paired Mac gets the banner, counted from 1 in pairing order. Default: the connected Mac, else the one seen most recently.
client_secretbodystringoptionalThe secret of a confidential client. It can instead go in a Basic Authorization header.
Example requestshell
curl -s -X POST "$RELAY/device_authorization" \
  -H "User-Agent: Herald-Agent/1.0" \
  -d client_id=hc_3fa91c0de27b45a6d1180123456789ab \
  -d scope=notify
Example responseJSON
{
  "device_code": "hrv_...",
  "user_code": "BDFG-HJKM",
  "verification_uri": "https://herald-relay.example.workers.dev/activate",
  "verification_uri_complete": "https://herald-relay.example.workers.dev/activate?user_code=BDFG-HJKM",
  "expires_in": 600,
  "interval": 5,
  "device_name": "Studio Mac",
  "device_count": 1,
  "device_index": 1,
  "device_online": true
}

Response fields

FieldTypeDescription
device_codestringThe secret the agent polls /token with. It begins hrv_.
user_codestringThe code the user matches in Herald: 8 consonants as XXXX-XXXX.
verification_uristringThe relay's /activate page, for a person with a browser.
verification_uri_completestringThe same address with the user code filled in.
expires_inintegerSeconds until the request expires: 600.
intervalintegerThe least seconds between polls: 5.
device_namestringThe name of the Mac that received the banner. Mac 1, Mac 2 and so on when the Mac has no name.
device_countintegerHow many Macs are paired with the relay.
device_indexintegerThe 1-based number of the chosen Mac.
device_onlinebooleantrue when that Mac is connected.

Errors

StatusWhen
400scope is not notify (invalid_scope), no Mac is paired, or device is out of range (invalid_request).
401The client is unknown or its secret is wrong (invalid_client).
429Approvals are already waiting, or too many were requested this hour (slow_down). The reply carries Retry-After: 60.
502The relay could not start the approval (temporarily_unavailable).

Notes

  • Tell the user which Mac will show the banner: device_name says so.
  • The user sees a banner with the code and Approve and Deny. Settings > Cloud > Connector approvals lists the request with the code in large type and, in small type, a 6-digit approval code for the web page.
  • Asking again with the same client takes over the open request. The earlier device_code stops working.
  • The request lasts 10 minutes. After approval, the agent has 5 minutes to collect the tokens.
  • At most 3 approvals wait on one Mac at once, and at most 12 are begun an hour.

GET /activate#

Serves the page where a person with a browser enters the agent's code and approves it. The page is optional: the user can approve from the banner in Herald instead. The page asks for the user_code the agent printed, then for the 6-digit approval code that Herald shows.

Request

NameInTypeRequiredDescription
user_codequerystringoptionalFills in the code field.
Example requestshell
curl -s "$RELAY/activate?user_code=BDFG-HJKM" -H "User-Agent: Herald-Agent/1.0"

Example response

The reply is an HTML page with status 200.

text
HTTP/2 200
content-type: text/html; charset=utf-8

<h1>Approve an agent</h1>

Errors

StatusWhen
500The relay's owner has not set its secret (misconfigured).

Notes

  • A wrong or expired user_code shows the entry page again with a note. It is never an error status.
  • The fifth wrong approval code denies the request.

The page posts to three helper routes. They belong to the page and are not an API for clients.

RouteWhat it does
POST /activateTakes user_code as a form and shows the confirmation step for the matching request.
POST /activate/approveTakes rid and code (the 6-digit approval code) as a form and approves.
POST /activate/denyTakes rid as a form and denies the request.

POST /token#

Collects the tokens of an approved device request. This is the device grant of the token endpoint. Send grant_type=urn:ietf:params:oauth:grant-type:device_code and poll no faster than interval. The other grants, the shared fields and the token reply are documented under POST /token.

Request

The body is a form.

NameInTypeRequiredDescription
grant_typebodystringrequiredurn:ietf:params:oauth:grant-type:device_code.
client_idbodystringrequiredThe client that started the request.
device_codebodystringrequiredThe hrv_ code from /device_authorization.
client_secretbodystringoptionalThe secret of a confidential client.
resourcebodystringoptionalThe MCP address. It must match the one the request was made for.
Example requestshell
curl -s -X POST "$RELAY/token" \
  -H "User-Agent: Herald-Agent/1.0" \
  -d grant_type=urn:ietf:params:oauth:grant-type:device_code \
  -d client_id=hc_3fa91c0de27b45a6d1180123456789ab \
  -d device_code=hrv_...

Example response

Once the user has approved, the reply is 200:

JSON
{
  "access_token": "hra_...",
  "token_type": "Bearer",
  "expires_in": 315360000,
  "refresh_token": "hrr_...",
  "scope": "notify"
}

While the user has not answered, the reply is 400:

JSON
{
  "error": "authorization_pending",
  "error_description": "waiting for the user to approve in Herald"
}

Response fields

The fields of the success reply are in POST /token.

Errors

A failed poll is 400 with {"error": "...", "error_description": "..."}, except client authentication, which is 401.

ErrorWhen
authorization_pendingThe user has not answered yet. Keep polling every interval seconds.
slow_downYou polled faster than interval. The interval grows by 5 seconds. Add that to yours.
access_deniedThe user pressed Deny, or entered five wrong approval codes. Stop.
expired_tokenThe request expired, the approval was not collected within 5 minutes, or the code is unknown. Start again at /device_authorization.
invalid_grantThe device_code is malformed, belongs to another client, or its connector was revoked.
invalid_targetresource does not match the request.
invalid_clientThe client is unknown or its secret is wrong. The status is 401.

Notes

  • Poll no faster than interval. The first poll must also wait interval seconds after the request started, or it gets slow_down.
  • A poll that gets slow_down or authorization_pending is not fatal. Only access_denied, expired_token and invalid_grant end the flow.
  • A repeated poll after approval, for example after a lost response, returns tokens again. It never undoes the approval.
  • Approval creates one agent key for the connector, named after client_name, with notify scope. It appears in Settings > Cloud > Agent keys and can be revoked there. The access token works on every agent endpoint and on /mcp.

Edit this page on GitHub

Esc
Getting started
Guides