Exchange a Steam Web API auth ticket for application tokens.
const url = 'https://native-api.sudomimus.com/direct-issue/steam-ticket';const options = { method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{"applicationAnchor":"example","steamTicketHex":"example","steamAppId":1}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://native-api.sudomimus.com/direct-issue/steam-ticket \ --header 'Content-Type: application/json' \ --data '{ "applicationAnchor": "example", "steamTicketHex": "example", "steamAppId": 1 }'The client SDK calls ISteamUser::GetAuthTicketForWebApi("sudomimus")
(the identity string MUST be exactly "sudomimus"), waits for the
GetTicketForWebApiResponse_t callback, hex-encodes the ticket
bytes, and POSTs them here together with the application anchor and
the Steam App ID. The server:
- Validates the application’s authentication-rule layer admits
STEAM_TICKETfor thissteamAppId. - Atomically records
sha256(steamTicketHex.toLowerCase())for replay protection (24-hour window); a duplicate is rejected with409. - Verifies the ticket with Steam’s
ISteamUserAuth/AuthenticateUserTicketendpoint, identity"sudomimus". - Validates the application’s realize-rule layer (
EMAIL/STEAM_ID/ACCOUNT_ALIAS/SECTOR_SUBJECT) and ensures aDIRECT_ISSUEreturn rule exists. - Issues access + refresh JWTs signed with the application’s private key.
Tokens follow the same shape as those issued through Connect’s
/redeem. Verification is the responsibility of the relying party
(see the Sudomimus.Token SDK).
The Layer-1 method name on the wire is STEAM_TICKET, but the
resolved identity is stored as a STEAM_ID64 Authentication row
— STEAM_TICKET and STEAM_OPENID share a single identity row
per SteamID64, differing only in how the SteamID64 was proven.
Request Body required
Section titled “Request Body required ”object
Public anchor identifying the integrating application.
Hex-encoded Steam Web API auth ticket bytes returned from
ISteamUser::GetAuthTicketForWebApi("sudomimus"). Case
insensitive — the server lowercases before hashing for replay
protection, but forwards the original bytes to Steam.
Steam App ID under which the ticket was generated. Must be
allow-listed by the application’s STEAM_TICKET authentication
rule. Tickets are bound to their issuing App ID server-side;
passing a different value will fail Steam verification.
Example generated
{ "applicationAnchor": "example", "steamTicketHex": "example", "steamAppId": 1}Responses
Section titled “ Responses ”Tokens issued.
object
Per-claim view across the three shareable claims, carried on both the 200 (why is a claim absent from the minted token) and the claim-gate 403 (what is still owed).
object
One shareable claim: what the application requests (requirement)
joined with the user’s standing decision (state). UNKNOWN means
the user was never asked; DENIED means the user explicitly
declined.
object
The developer’s policy for the claim. SYNTHETIC guarantees the
claim is present but permits a generated placeholder (a stand-in
name, a proxy email) when the user has not shared real data —
unlike REQUIRED it never blocks issuance or raises an errand.
One shareable claim: what the application requests (requirement)
joined with the user’s standing decision (state). UNKNOWN means
the user was never asked; DENIED means the user explicitly
declined.
object
The developer’s policy for the claim. SYNTHETIC guarantees the
claim is present but permits a generated placeholder (a stand-in
name, a proxy email) when the user has not shared real data —
unlike REQUIRED it never blocks issuance or raises an errand.
One shareable claim: what the application requests (requirement)
joined with the user’s standing decision (state). UNKNOWN means
the user was never asked; DENIED means the user explicitly
declined.
object
The developer’s policy for the claim. SYNTHETIC guarantees the
claim is present but permits a generated placeholder (a stand-in
name, a proxy email) when the user has not shared real data —
unlike REQUIRED it never blocks issuance or raises an errand.
Short-lived access token (JWT). Body shape matches Connect’s
AccessTokenBody: the application-visible user key is the
subject (sector subject) claim, not a raw account identifier.
The firstName / lastName / emailAddress claims are
consent-gated and may be absent (see Connect AccessTokenBody).
Long-lived refresh token (JWT). Use Connect’s /refresh to obtain a new access token without re-acquiring a Steam ticket.
Example
{ "claims": { "email": { "requirement": "OFF", "state": "UNKNOWN" }, "firstName": { "requirement": "OFF", "state": "UNKNOWN" }, "lastName": { "requirement": "OFF", "state": "UNKNOWN" } }}Malformed request (e.g. invalid steamAppId).
Error response body. The Native service emits { "reason": "<SymbolDescription>" }
for known failure modes. When the reason symbol’s description begins with
PRIVATE, the body is empty (zero bytes) and only the HTTP status carries
signal — both reason and the body itself are absent in that case.
object
Stable machine-readable reason code.
Example generated
{ "reason": "example"}Steam rejected the ticket as invalid.
Error response body. The Native service emits { "reason": "<SymbolDescription>" }
for known failure modes. When the reason symbol’s description begins with
PRIVATE, the body is empty (zero bytes) and only the HTTP status carries
signal — both reason and the body itself are absent in that case.
object
Stable machine-readable reason code.
Example generated
{ "reason": "example"}The attempt was refused. The response reason distinguishes:
Layer1Denied,Layer2Denied,Layer3Denied— the application’s three-layer rules rejected this attempt (which layer is indicated by the reason code).ApplicationDisabled— the application is disabled.AccountDisabled— the resolved account is disabled.AccountDeleted— the resolved account has been erased.ClaimConsentRequired— the application requires a claim the user has not yet granted; direct-issue cannot prompt.RequiredClaimDataMissing— every required claim is granted, but the account lacks the underlying data (e.g. no email on a Steam-created account).
For the two claim-gate reasons the body additionally carries a
per-claim claims view (UNKNOWN distinguishes “never asked”
from DENIED “declined”) and an errand handoff — a short-lived
browser URL where the user can authenticate, complete the
missing data, and grant consent. Open errand.url in the
system browser, poll GET /errand/{errandKey}/status (or wait
for the user), then retry this request once.
Repeated blocked calls reuse the live ticket: while it has at
least 15 minutes left and the owed task scope is unchanged, the
same errandKey (with its original expiresAt) is returned
again, so retries cannot split the handoff state.
403 body. For the claim-gate reasons (ClaimConsentRequired,
RequiredClaimDataMissing) the claims view and the errand
handoff are present; for every other reason only reason is set.
object
Stable machine-readable reason code.
Per-claim view across the three shareable claims, carried on both the 200 (why is a claim absent from the minted token) and the claim-gate 403 (what is still owed).
object
One shareable claim: what the application requests (requirement)
joined with the user’s standing decision (state). UNKNOWN means
the user was never asked; DENIED means the user explicitly
declined.
object
The developer’s policy for the claim. SYNTHETIC guarantees the
claim is present but permits a generated placeholder (a stand-in
name, a proxy email) when the user has not shared real data —
unlike REQUIRED it never blocks issuance or raises an errand.
One shareable claim: what the application requests (requirement)
joined with the user’s standing decision (state). UNKNOWN means
the user was never asked; DENIED means the user explicitly
declined.
object
The developer’s policy for the claim. SYNTHETIC guarantees the
claim is present but permits a generated placeholder (a stand-in
name, a proxy email) when the user has not shared real data —
unlike REQUIRED it never blocks issuance or raises an errand.
One shareable claim: what the application requests (requirement)
joined with the user’s standing decision (state). UNKNOWN means
the user was never asked; DENIED means the user explicitly
declined.
object
The developer’s policy for the claim. SYNTHETIC guarantees the
claim is present but permits a generated placeholder (a stand-in
name, a proxy email) when the user has not shared real data —
unlike REQUIRED it never blocks issuance or raises an errand.
The browser side-trip that unblocks a claim-gated direct-issue:
a short-lived (30 minutes), single-use bearer URL where the user
authenticates (when account data is being written), completes any
missing data, and grants consent. The stored task list is server-
side only — open the URL and let the page drive. Repeated blocked
calls re-hand the same live handoff (same errandKey, original
expiresAt) while it has at least 15 minutes left and the owed
task scope is unchanged.
object
Bearer key (ernd_…); also the status-poll path key.
Open this in the user’s system browser.
Example
{ "claims": { "email": { "requirement": "OFF", "state": "UNKNOWN" }, "firstName": { "requirement": "OFF", "state": "UNKNOWN" }, "lastName": { "requirement": "OFF", "state": "UNKNOWN" } }}Application anchor not found.
Error response body. The Native service emits { "reason": "<SymbolDescription>" }
for known failure modes. When the reason symbol’s description begins with
PRIVATE, the body is empty (zero bytes) and only the HTTP status carries
signal — both reason and the body itself are absent in that case.
object
Stable machine-readable reason code.
Example generated
{ "reason": "example"}Replay protection conflict — this Steam ticket has already been
redeemed within the replay window. Acquire a fresh ticket via
GetAuthTicketForWebApi and retry.
Error response body. The Native service emits { "reason": "<SymbolDescription>" }
for known failure modes. When the reason symbol’s description begins with
PRIVATE, the body is empty (zero bytes) and only the HTTP status carries
signal — both reason and the body itself are absent in that case.
object
Stable machine-readable reason code.
Example generated
{ "reason": "example"}Steam’s verification endpoint was unreachable or returned an unparseable response.
Error response body. The Native service emits { "reason": "<SymbolDescription>" }
for known failure modes. When the reason symbol’s description begins with
PRIVATE, the body is empty (zero bytes) and only the HTTP status carries
signal — both reason and the body itself are absent in that case.
object
Stable machine-readable reason code.
Example generated
{ "reason": "example"}default
Section titled “default ”Error response.
Error response body. The Native service emits { "reason": "<SymbolDescription>" }
for known failure modes. When the reason symbol’s description begins with
PRIVATE, the body is empty (zero bytes) and only the HTTP status carries
signal — both reason and the body itself are absent in that case.
object
Stable machine-readable reason code.
Example generated
{ "reason": "example"}