SimTX API v24/Device login
Device login is the sign-in flow for software that cannot, or should not, handle a SimTX password itself: desktop applications, command-line tools and headless bridges. The software shows the user a short code, the user approves it in a browser while signed in to SimTX, and the software receives an access and refresh credential.
The flow is modelled on the OAuth 2.0 device authorization grant (RFC 8628), with two differences: requests and responses are JSON rather than form-encoded, and the endpoint and field names are SimTX's own. Generic OAuth libraries will therefore not work without adaptation.
Software that only receives does not need to sign in at all. Bots that run unattended should normally use a personal credential instead (see Authentication).
Sequence[edit | edit source]
Client auth.simtx.net User's browser
| POST /device/authorizations | |
|---------------------------------->| |
| 201 deviceCode, userCode, URI | |
|<----------------------------------| |
| show userCode, open URI ------------------------------------------->|
| | user signs in and approves |
| POST /device/token (every 5 s) |<---------------------------------|
|---------------------------------->| |
| 400 authorization_pending | |
|<----------------------------------| |
| POST /device/token | |
|---------------------------------->| |
| 200 access + refresh credential | |
|<----------------------------------| |
Starting a login[edit | edit source]
POST /api/v1/device/authorizations. No authentication.
Request[edit | edit source]
{ "clientName": "Example SDR Client", "scope": "game.transmit" }
| Field | Type | Required | Description |
|---|---|---|---|
clientName |
string | No | Name shown to the user on the approval page and in their device list. Truncated to 80 characters. Defaults to SimTX client when blank.
|
scope |
string | No | Space-separated list of permission identifiers the credential may use. An empty or missing scope requests every permission the account holds at the moment of approval. Unknown identifiers are rejected with 400 scope_invalid.
|
Request the smallest scope that does the job. A transmitter needs game.transmit. A tool that publishes extensions needs catalog.publish.
Response[edit | edit source]
201 Created:
{
"deviceCode": "stx_dc1_9a1f…",
"userCode": "QZ7K-M4TP",
"verificationUri": "https://auth.simtx.net/device",
"verificationUriComplete": "https://auth.simtx.net/device?code=QZ7K-M4TP",
"expiresIn": 600,
"interval": 5,
"expiresAt": "2026-10-24T12:10:00Z"
}
| Field | Type | Description |
|---|---|---|
deviceCode |
string | Secret used to poll for the result. Keep it private; do not show it to the user. |
userCode |
string | Code the user confirms in the browser. |
verificationUri |
string | Page where the user types the code. |
verificationUriComplete |
string | Same page with the code pre-filled. Open this one if the client can launch a browser. |
expiresIn |
integer | Seconds until the device code expires (600). |
interval |
integer | Minimum seconds between polls (5). |
expiresAt |
string | Absolute expiry time. |
Display userCode prominently and open verificationUriComplete. The user can then compare the code on the page with the code in the application before approving.
Polling for credentials[edit | edit source]
POST /api/v1/device/token. No authentication.
{ "deviceCode": "stx_dc1_9a1f…" }
Success[edit | edit source]
200 OK with a credential pair:
{
"accessCredential": "stx_at1_5c0e…",
"refreshCredential": "stx_rt1_b77d…",
"credentialType": "Bearer",
"expiresIn": 600,
"scope": "game.transmit"
}
| Field | Type | Description |
|---|---|---|
accessCredential |
string | Bearer credential, valid for expiresIn seconds.
|
refreshCredential |
string | Used once to obtain the next pair. |
credentialType |
string | Always Bearer.
|
expiresIn |
integer | Access credential lifetime in seconds (600). |
scope |
string | Space-separated scopes actually granted. |
A device code can be exchanged exactly once. Polling again after success returns access_denied.
Not ready yet[edit | edit source]
Until the user acts, the service answers 400 with one of the following codes. Every one of these responses includes the interval the client must now use, as a string, in fieldErrors.interval.
code |
Meaning | Client action |
|---|---|---|
authorization_pending |
The user has not approved or denied yet. | Wait interval seconds and poll again.
|
slow_down |
The poll arrived sooner than interval seconds after the previous one. The interval has been increased by 5 seconds. |
Adopt fieldErrors.interval for all later polls.
|
expired_token |
Ten minutes have passed. | Start a new login. |
access_denied |
The user denied the request, or the code was already exchanged. | Stop. |
invalid_grant |
The device code is unknown. | Start a new login. |
A client that ignores slow_down and keeps its old pace is told to slow down on every single poll, and each warning adds another five seconds. Such a client never receives credentials. Read the interval from the response.
Refreshing[edit | edit source]
POST /api/v1/device/refresh. No authentication.
{ "refreshCredential": "stx_rt1_b77d…" }
On success the response is 200 OK with a new credential pair in the same format as above. The refresh credential rotates on every use. Store the new one immediately and discard the old one.
| HTTP | code |
Meaning |
|---|---|---|
| 401 | invalid_grant |
The refresh credential is unknown, expired, or its device session was revoked. Sign in again. |
| 401 | refresh_reuse_detected |
A refresh credential that had already been used was presented again. The service treats this as theft and revokes the whole device session. Sign in again. |
Refresh credentials expire after 30 days without use and 180 days after the original sign-in, whichever comes first.
Recommended client behaviour[edit | edit source]
- On start-up, load the stored refresh credential. If there is none, run device login.
- Refresh to obtain an access credential. Keep the access credential in memory.
- Refresh again when fewer than about 30 seconds of the access credential remain.
- When any endpoint answers
401 credential_invalid, refresh once and repeat the request. If the refresh fails, delete the stored credential and run device login. - Serialise refreshes. Two threads refreshing with the same refresh credential at once will trigger
refresh_reuse_detected, and the user will have to sign in again.
Example[edit | edit source]
A complete login with curl and jq:
AUTH=https://auth.simtx.net
start=$(curl -s -X POST "$AUTH/api/v1/device/authorizations" \
-H 'Content-Type: application/json' \
-d '{"clientName":"curl demo","scope":"game.transmit"}')
device=$(jq -r .deviceCode <<<"$start")
interval=$(jq -r .interval <<<"$start")
echo "Open $(jq -r .verificationUriComplete <<<"$start") and confirm $(jq -r .userCode <<<"$start")"
while sleep "$interval"; do
reply=$(curl -s -X POST "$AUTH/api/v1/device/token" \
-H 'Content-Type: application/json' -d "{\"deviceCode\":\"$device\"}")
case $(jq -r '.code // "ok"' <<<"$reply") in
ok) jq . <<<"$reply"; break ;;
authorization_pending) ;;
slow_down) interval=$(jq -r .fieldErrors.interval <<<"$reply") ;;
*) echo "login failed: $reply"; exit 1 ;;
esac
done
See also[edit | edit source]
- SimTX API v24/Account service: errors, credential formats and rate limits.
- SimTX API v24/Game tickets: the next step for a client that wants to transmit.