Quickstart
Go from an invite to your first model call with the CLI or raw HTTP.
Go from an invite to your first model call. Use the CLI if you just want to make requests; drop to raw HTTP when you are building a client.
Fastest path — the CLI
The CLI runs the whole device flow for you and stores device-bound credentials, so you can authenticate once and skip the raw HTTP handshake below. Install it with Homebrew:
brew install sovereign46/tap/s46# Opens your browser, approves the device, and stores credentials
s46 login --email you@s46.devBuild it yourself — raw HTTP
If you are writing your own client, here is the same flow over plain HTTP. Five steps: start → approve → poll → identify → call a model.
Start the device login
Request a device code. You get back a short userCode for the human to approve and a deviceCode your client polls with.
curl -X POST https://api.s46.dev/v1/auth/device/start \
-H "Content-Type: application/json" \
-d '{
"email": "you@s46.dev",
"deviceId": "dev-laptop",
"deviceName": "Dev laptop"
}'const res = await fetch("https://api.s46.dev/v1/auth/device/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "you@s46.dev",
deviceId: "dev-laptop",
deviceName: "Dev laptop",
}),
});
const start = await res.json();
console.log(start.verificationUri); // send the user hereimport requests
res = requests.post("https://api.s46.dev/v1/auth/device/start", json={
"email": "you@s46.dev",
"deviceId": "dev-laptop",
"deviceName": "Dev laptop",
})
start = res.json()
print(start["verificationUri"]) # send the user here{
"deviceCode": "s46_device_...",
"userCode": "WXYZ-1234",
"verificationUri": "https://api.s46.dev/v1/auth/magic/consume?token=...",
"intervalSeconds": 2,
"expiresAt": "2030-01-01T12:00:00Z"
}Approve in the browser
Open verificationUri. The user signs in via magic link or passkey and approves the pending device. The GET page validates but does not consume the token, so email scanners can't approve a login on the user's behalf.
Poll for the token set
Poll no faster than intervalSeconds. 428 authorization_pending means keep waiting; 200 returns your credentials. Polling too fast earns 429 rate_limited.
curl -X POST https://api.s46.dev/v1/auth/device/poll \
-H "Content-Type: application/json" \
-d '{ "deviceCode": "s46_device_..." }'async function poll(deviceCode, intervalSeconds) {
for (;;) {
const res = await fetch("https://api.s46.dev/v1/auth/device/poll", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ deviceCode }),
});
if (res.status === 200) return res.json(); // token set
if (res.status !== 428) throw new Error(await res.text());
await new Promise((r) => setTimeout(r, intervalSeconds * 1000));
}
}
const tokens = await poll(start.deviceCode, start.intervalSeconds);import time
while True:
res = requests.post("https://api.s46.dev/v1/auth/device/poll",
json={"deviceCode": start["deviceCode"]})
if res.status_code == 200:
tokens = res.json()
break
if res.status_code != 428:
res.raise_for_status()
time.sleep(start["intervalSeconds"]){
"account": "you@s46.dev",
"organization": "s46",
"team": "@s46/engineering",
"deviceId": "dev-laptop",
"accessToken": "s46_access_...",
"refreshToken": "s46_refresh_...",
"expiresAt": "2030-01-01T13:00:00Z"
}Identify the caller
Use the access token as a bearer credential. GET /v1/me returns the identity, team, role, and region behind the token — this is your first authenticated control-plane request.
curl https://api.s46.dev/v1/me \
-H "Authorization: Bearer $S46_ACCESS_TOKEN"const me = await fetch("https://api.s46.dev/v1/me", {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
}).then((r) => r.json());
// → { email, organization, team, role, region, deviceId }me = requests.get(
"https://api.s46.dev/v1/me",
headers={"Authorization": f"Bearer {tokens['accessToken']}"},
).json()Make your first model call
Call the gateway with the same bearer token. The gateway resolves your team, enforces model policy, and picks the team default when you omit model. The routes are OpenAI-, Anthropic-, and Codex-compatible.
curl -X POST https://gateway.s46.dev/v1/chat/completions \
-H "Authorization: Bearer $S46_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "s46/devstral-small-2-24b",
"messages": [
{ "role": "user", "content": "Write a short status update." }
]
}'// Drop-in for the OpenAI SDK — just swap the base URL.
import OpenAI from "openai";
const client = new OpenAI({
apiKey: tokens.accessToken,
baseURL: "https://gateway.s46.dev/v1",
});
const completion = await client.chat.completions.create({
model: "s46/devstral-small-2-24b",
messages: [{ role: "user", content: "Write a short status update." }],
});from openai import OpenAI
client = OpenAI(
api_key=tokens["accessToken"],
base_url="https://gateway.s46.dev/v1",
)
completion = client.chat.completions.create(
model="s46/devstral-small-2-24b",
messages=[{"role": "user", "content": "Write a short status update."}],
)