s46.apiv1

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.dev

Build 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.

1

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 here
import 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"
}
2

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.

3

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"
}
4

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()
5

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."}],
)

Where to go next

On this page