Device Activation API

REST API endpoints for the device activation flow. Allows agents and external systems to activate devices without interactive browser-based OAuth, using a code-based confirmation mechanism via the mobile app.

Device activation vs. agent permissions. This flow gets a key into an agent’s hands — the user pairs a device and an API key is issued, bound to a workspace. Narrowing what that key may do is a separate concern, covered in Agent Permissions: an agent can declare the scope set it actually needs and have the user approve exactly that, which is how an over-broad key gets tightened.

Overview

The device activation flow is designed for non-interactive environments (agents, CI/CD, headless systems):

  1. Agent initiates → Gets a code (expires in 10 minutes)
  2. User enters code in mobile app → App confirms the code
  3. Agent polls → Retrieves the issued API key (one-time read)
  4. Agent uses key → All future requests use the API key

Start Device Activation

Begin the device activation flow. This is a public endpoint requiring no authentication.

POST /api/v1/activate

Rate Limiting

This endpoint is rate-limited per client IP. Allow at least 1 request per second.

Example

curl -X POST https://clanker.net/api/v1/activate \
  -H "Content-Type: application/json"

Response

{
  "device_code": "8c47…opaque…",
  "user_code": "ABCD-1234",
  "verification_url": "https://clanker.net/activate",
  "verification_url_complete": "https://clanker.net/activate?code=ABCD-1234",
  "app_download_url": "https://clanker.net/app",
  "expires_in": 600,
  "interval": 2
}

Response Fields

FieldTypeDescription
device_codestringOpaque token the CLI uses for /poll. Never shown to humans.
user_codestring8-character display code (format: XXXX-XXXX). Show this to the user.
verification_urlstringURL where the user enters the code on the mobile app or web.
verification_url_completestringPre-filled approval URL (?code=…) for browser-based prefill.
app_download_urlstringURL for downloading the Clanker mobile app.
expires_innumberSeconds until the code expires (600 = 10 minutes).
intervalnumberMinimum seconds between /poll calls.

Poll Activation Status

Poll to check if the user has confirmed the activation code via the mobile app.

POST /api/v1/activate/poll

Public Endpoint

This endpoint requires no authentication. Rate-limit: up to once per second per code.

Request Body

FieldTypeRequiredDescription
device_codestringYesThe opaque device_code from the POST /api/v1/activate response — not the human user_code.

Poll with the device_code, the secret token the agent holds. The user_code (e.g. ABCD-1234) is only for the human to type into the app; polling with it returns expired.

Example

curl -X POST https://clanker.net/api/v1/activate/poll \
  -H "Content-Type: application/json" \
  -d '{"device_code": "9A98Btdj…opaque…"}'

Poll always answers 200 with a status. Only approved carries a key.

statusMeaningWhat to do
pendingThe user has not answered yetKeep polling
approvedApproved, and this call is the one that got the keyStop; store the key
deniedThe user rejected it in the appStop; do not retry
consumedApproved, but the key was already handed to an earlier pollStop; start a new activation
expiredPast its 10-minute TTL, or the code was never validStop; start a new activation

Response (Pending)

{
  "status": "pending"
}

Response (Completed)

User confirmed in the app. One-time read — whichever poll claims it first is the only one that ever sees the plaintext key.

{
  "status": "approved",
  "api_key": "ck_live_...",
  "tier": "paygmode",
  "workspace": { "id": "7c2e…", "name": "Acme" }
}
FieldTypeDescription
statusstring"approved" — the row’s state. A "completed" alias existed for older CLIs and has been removed.
api_keystringThe issued API key. Use this for all future requests.
tierstringUser’s subscription mode: "byokmode", "paygmode", or "basedmode".
workspaceobjectThe workspace the key is bound to. Absent when the key is not workspace-bound; name may be null.

The key is bound to a workspace at issuance — whichever workspace the approving app was in — and cannot be repointed later. That is why the poll response tells you which one you got.

Response (Denied / Expired)

{
  "status": "denied"
}

A denial is distinct from an expiry on purpose: the CLI can say “the user said no” instead of waiting out a timeout.

Polling Strategy

  • Recommended interval: 2–5 seconds
  • Timeout: Stop polling after 10 minutes (when code expires)
  • Backoff: Optional exponential backoff to reduce load (max ~1 request/sec)

Confirm Activation (Mobile)

Confirm an activation code from the mobile app. This endpoint is called by the Clanker mobile app when the user enters a code in Settings → External Agents → Activate.

POST /api/v1/activate/confirm

Authentication

Requires authentication via x-auth-token (from mobile app session).

curl -X POST https://clanker.net/api/v1/activate/confirm \
  -H "x-auth-token: YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"code": "ABCD-1234"}'

Request Body

FieldTypeRequiredDescription
codestringYesThe activation code from the user input.

Response

{
  "success": true
}

Send x-workspace-id to bind the new key to a workspace other than your personal one. You must be a member of it.

Errors

Statuserror.codeDescription
400INVALID_INPUTRequest body is missing the code field
401UNAUTHORIZEDMissing or invalid x-auth-token header
403FORBIDDENx-workspace-id names a workspace you are not a member of
404NOT_FOUNDNo such code
409CONFLICTCode is no longer pending — message names the state it is already in
410EXPIREDCode passed its 10-minute TTL
500INTERNAL_ERRORThe API key could not be minted

Approve and deny race through a single compare-and-set, so exactly one of them wins and the loser gets the 409 describing the state that won.


Lookup Pending Code (Mobile)

Returns metadata about a pending code so the approval UI can show what the user is about to authorize (client label, origin IP, user agent, status). Used by the mobile app when the activation flow is initiated from a URL prefill (e.g. opening the CLI’s clanker://activate?code=… link).

GET /api/v1/activate/lookup?code=XXXX1234

Authentication: x-auth-token.

Response:

{
  "user_code": "XXXX1234",
  "client_label": "openclaw / dev-laptop",
  "client_ua": "openclaw/0.4 (macOS 14.4)",
  "client_ip": "203.0.113.42",
  "status": "pending",
  "expires_at": "2026-06-01T15:20:00.000Z"
}

status is one of pending | approved | denied | expired | consumed.


Deny Activation (Mobile)

Mark a pending activation as denied so the polling CLI receives a clean denial instead of an opaque expiration.

POST /api/v1/activate/deny

Authentication: x-auth-token. Body: { "code": "XXXX-1234" }. Response: { "success": true }.


End-to-End Flow Example

Step 1: Agent initiates activation

ACTIVATE_RESPONSE=$(curl -s -X POST https://clanker.net/api/v1/activate)
USER_CODE=$(echo $ACTIVATE_RESPONSE | jq -r '.user_code')      # shown to the human
DEVICE_CODE=$(echo $ACTIVATE_RESPONSE | jq -r '.device_code')  # used for polling
echo "Enter this code in the Clanker app: $USER_CODE"

Output:

Enter this code in the Clanker app: ABCD-1234

Step 2: User opens the Clanker mobile app

  1. Open the app
  2. Tap Account tab
  3. Tap Activate Device
  4. Enter the code: ABCD-1234
  5. Tap Confirm

The app calls POST /api/v1/activate/confirm with the code.

Step 3: Agent polls for completion

while true; do
  POLL_RESPONSE=$(curl -s -X POST https://clanker.net/api/v1/activate/poll \
    -H "Content-Type: application/json" \
    -d "{\"device_code\": \"$DEVICE_CODE\"}")

  STATUS=$(echo $POLL_RESPONSE | jq -r '.status')

  if [ "$STATUS" = "approved" ]; then
    API_KEY=$(echo $POLL_RESPONSE | jq -r '.api_key')
    echo "Activation complete! API Key: $API_KEY"
    break
  elif [ "$STATUS" = "denied" ]; then
    echo "Denied in the app."
    exit 1
  elif [ "$STATUS" = "expired" ] || [ "$STATUS" = "consumed" ]; then
    echo "Code is no longer usable. Try again."
    exit 1
  fi

  sleep 2
done

Output:

Activation complete! API Key: ck_live_abc123def456...

Step 4: Agent uses the API key

All future requests use the x-api-key header:

curl https://clanker.net/api/v1/marketplace/skills \
  -H "x-api-key: ck_live_abc123def456..."

Implementation Notes

Code Format

Activation codes are 8 characters in format XXXX-XXXX:

  • Character set: A-Z (no I/O to avoid confusion with numbers), 2-9 (no 0/1)
  • Randomness: Generated from cryptographic random bytes
  • Case-insensitive: Normalized to uppercase by the server

Example codes: ABCD-1234, MXYZ-5678, WXYZ-9ABC

TTL (Time-To-Live)

ResourceTTLNotes
Activation code10 minutes (600 seconds)expires_in in the create response
Key delivery entryThe code’s remaining lifetimeWritten on approval, claimed by the first poll

Approval does not extend the clock. A code approved at 9m30s leaves 30 seconds to collect the key.

State Machine

                  ┌──(user denies)──────────────→ [Denied]
[Pending] ────────┤
   │              └──(user confirms)──→ [Approved] ──(first poll claims key)──→ [Consumed]

   └──(10 min elapses)──────────────────────────→ [Expired]

denied, consumed and expired are all terminal. Poll reports approved for the single call that claims the key from the approved state; every later poll on the same code sees consumed.

API Key Generation

When a code is confirmed:

  1. A new API key is generated with prefix ck_live_, named after the device label and the user code
  2. The key is bound to the approving workspace at creation
  3. The plaintext key is returned once in the poll response
  4. The claim is atomic, so a racing second poller never sees the plaintext
  5. Polling again with the same code returns { status: "consumed" }

Error Handling

Rate Limiting (429)

If you receive a 429 error on POST /api/v1/activate, wait before retrying. Recommended: retry after 5 seconds with exponential backoff.

Invalid Request (400)

Missing required fields:

{
  "error": {
    "code": "INVALID_INPUT",
    "message": "Missing activation code"
  }
}

Every error on this surface uses that envelope. Branch on error.code — the message is prose and may be reworded or translated.

Code Validation

The server normalizes codes (uppercase, trim) but validation is lenient:

# These all work:
{ "code": "ABCD-1234" }
{ "code": "abcd-1234" }
{ "code": "  ABCD-1234  " }

Security Notes

  1. Codes are short-lived: 10-minute expiration prevents brute force
  2. One-time read: API key is deleted after first retrieval
  3. Rate-limited: Per-IP rate limiting on activation creation
  4. User-authenticated: Confirmation endpoint requires valid x-auth-token
  5. No PII in logs: Codes are not logged (only stored in ephemeral memory cache)

Troubleshooting

IssueSolution
Poll returns expiredCreate a new code with POST /api/v1/activate (10-minute timeout)
Poll returns consumedThe key was already handed to an earlier poll — it is not re-issuable. Create a new code.
Poll returns deniedThe user rejected it. Ask them, then create a new code.
Poll returns pending indefinitelyThe user has not confirmed. Check mobile app notifications.
Poll returns expired immediatelyYou are probably polling with the user_code. Poll with device_code.
Confirm returns 409Another approve/deny already resolved the code; message names the winning state.
Rate limitedWait 5+ seconds before creating another code.