Start a device link
curl --request POST \
--url http://localhost:3000/api/v1/device-links \
--header 'Content-Type: application/json' \
--data '
{
"verifierHash": "aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs",
"deviceName": "work-laptop",
"clientVersion": "0.1.0",
"osFamily": "darwin",
"arch": "arm64"
}
'import requests
url = "http://localhost:3000/api/v1/device-links"
payload = {
"verifierHash": "aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs",
"deviceName": "work-laptop",
"clientVersion": "0.1.0",
"osFamily": "darwin",
"arch": "arm64"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
verifierHash: 'aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs',
deviceName: 'work-laptop',
clientVersion: '0.1.0',
osFamily: 'darwin',
arch: 'arm64'
})
};
fetch('http://localhost:3000/api/v1/device-links', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "3000",
CURLOPT_URL => "http://localhost:3000/api/v1/device-links",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'verifierHash' => 'aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs',
'deviceName' => 'work-laptop',
'clientVersion' => '0.1.0',
'osFamily' => 'darwin',
'arch' => 'arm64'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "http://localhost:3000/api/v1/device-links"
payload := strings.NewReader("{\n \"verifierHash\": \"aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs\",\n \"deviceName\": \"work-laptop\",\n \"clientVersion\": \"0.1.0\",\n \"osFamily\": \"darwin\",\n \"arch\": \"arm64\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("http://localhost:3000/api/v1/device-links")
.header("Content-Type", "application/json")
.body("{\n \"verifierHash\": \"aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs\",\n \"deviceName\": \"work-laptop\",\n \"clientVersion\": \"0.1.0\",\n \"osFamily\": \"darwin\",\n \"arch\": \"arm64\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://localhost:3000/api/v1/device-links")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"verifierHash\": \"aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs\",\n \"deviceName\": \"work-laptop\",\n \"clientVersion\": \"0.1.0\",\n \"osFamily\": \"darwin\",\n \"arch\": \"arm64\"\n}"
response = http.request(request)
puts response.read_body{
"publicRequestId": "<string>",
"approvalUrl": "<string>",
"expiresAt": "2023-11-07T05:31:56Z",
"pollIntervalMs": 1500
}{
"error": {
"code": "SYNC_WEEKLY_LIMIT_REACHED",
"message": "Cloud report limit reached. This scan stays local.",
"requestId": "8f14e45f-ceea-467a-9f8b-2b6b2b6a1f2c",
"details": {
"weekly": {
"used": 15,
"limit": 15,
"resetsAt": "2026-08-24T06:00:00.000Z"
}
}
}
}{
"error": {
"code": "SYNC_WEEKLY_LIMIT_REACHED",
"message": "Cloud report limit reached. This scan stays local.",
"requestId": "8f14e45f-ceea-467a-9f8b-2b6b2b6a1f2c",
"details": {
"weekly": {
"used": 15,
"limit": 15,
"resetsAt": "2026-08-24T06:00:00.000Z"
}
}
}
}Device linking
Start a device link
Unauthenticated bootstrap. The CLI keeps a verifier locally and sends only its SHA-256 hash. The response carries an opaque public request id and a browser approval URL — never a bearer credential. Link requests live 10 minutes.
The device name, client version, OS family, and architecture are sanitized server-side before storage.
POST
/
api
/
v1
/
device-links
Start a device link
curl --request POST \
--url http://localhost:3000/api/v1/device-links \
--header 'Content-Type: application/json' \
--data '
{
"verifierHash": "aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs",
"deviceName": "work-laptop",
"clientVersion": "0.1.0",
"osFamily": "darwin",
"arch": "arm64"
}
'import requests
url = "http://localhost:3000/api/v1/device-links"
payload = {
"verifierHash": "aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs",
"deviceName": "work-laptop",
"clientVersion": "0.1.0",
"osFamily": "darwin",
"arch": "arm64"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
verifierHash: 'aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs',
deviceName: 'work-laptop',
clientVersion: '0.1.0',
osFamily: 'darwin',
arch: 'arm64'
})
};
fetch('http://localhost:3000/api/v1/device-links', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "3000",
CURLOPT_URL => "http://localhost:3000/api/v1/device-links",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'verifierHash' => 'aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs',
'deviceName' => 'work-laptop',
'clientVersion' => '0.1.0',
'osFamily' => 'darwin',
'arch' => 'arm64'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "http://localhost:3000/api/v1/device-links"
payload := strings.NewReader("{\n \"verifierHash\": \"aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs\",\n \"deviceName\": \"work-laptop\",\n \"clientVersion\": \"0.1.0\",\n \"osFamily\": \"darwin\",\n \"arch\": \"arm64\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("http://localhost:3000/api/v1/device-links")
.header("Content-Type", "application/json")
.body("{\n \"verifierHash\": \"aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs\",\n \"deviceName\": \"work-laptop\",\n \"clientVersion\": \"0.1.0\",\n \"osFamily\": \"darwin\",\n \"arch\": \"arm64\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://localhost:3000/api/v1/device-links")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"verifierHash\": \"aGFzaC1vZi10aGUtdmVyaWZpZXItYmFzZTY0dXJs\",\n \"deviceName\": \"work-laptop\",\n \"clientVersion\": \"0.1.0\",\n \"osFamily\": \"darwin\",\n \"arch\": \"arm64\"\n}"
response = http.request(request)
puts response.read_body{
"publicRequestId": "<string>",
"approvalUrl": "<string>",
"expiresAt": "2023-11-07T05:31:56Z",
"pollIntervalMs": 1500
}{
"error": {
"code": "SYNC_WEEKLY_LIMIT_REACHED",
"message": "Cloud report limit reached. This scan stays local.",
"requestId": "8f14e45f-ceea-467a-9f8b-2b6b2b6a1f2c",
"details": {
"weekly": {
"used": 15,
"limit": 15,
"resetsAt": "2026-08-24T06:00:00.000Z"
}
}
}
}{
"error": {
"code": "SYNC_WEEKLY_LIMIT_REACHED",
"message": "Cloud report limit reached. This scan stays local.",
"requestId": "8f14e45f-ceea-467a-9f8b-2b6b2b6a1f2c",
"details": {
"weekly": {
"used": 15,
"limit": 15,
"resetsAt": "2026-08-24T06:00:00.000Z"
}
}
}
}Body
application/json
⌘I