跳转到内容

Exchange a Steam Web API auth ticket for application tokens.

POST
/direct-issue/steam-ticket
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:

  1. Validates the application’s authentication-rule layer admits STEAM_TICKET for this steamAppId.
  2. Atomically records sha256(steamTicketHex.toLowerCase()) for replay protection (24-hour window); a duplicate is rejected with 409.
  3. Verifies the ticket with Steam’s ISteamUserAuth/AuthenticateUserTicket endpoint, identity "sudomimus".
  4. Validates the application’s realize-rule layer (EMAIL / STEAM_ID / ACCOUNT_ALIAS / SECTOR_SUBJECT) and ensures a DIRECT_ISSUE return rule exists.
  5. 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.

Media type application/json
object
applicationAnchor
required

Public anchor identifying the integrating application.

string
steamTicketHex
required

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.

string
steamAppId
required

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.

integer format: int64
>= 1
Example generated
{
"applicationAnchor": "example",
"steamTicketHex": "example",
"steamAppId": 1
}

Tokens issued.

Media type application/json
object
claims
required

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
email
required

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
requirement
required

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.

string
Allowed values: OFF OPTIONAL REQUIRED SYNTHETIC
state
required
string
Allowed values: UNKNOWN GRANTED DENIED
firstName
required

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
requirement
required

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.

string
Allowed values: OFF OPTIONAL REQUIRED SYNTHETIC
state
required
string
Allowed values: UNKNOWN GRANTED DENIED
lastName
required

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
requirement
required

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.

string
Allowed values: OFF OPTIONAL REQUIRED SYNTHETIC
state
required
string
Allowed values: UNKNOWN GRANTED DENIED
applicationAnchor
required
string
accessToken
required

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

string
refreshToken
required

Long-lived refresh token (JWT). Use Connect’s /refresh to obtain a new access token without re-acquiring a Steam ticket.

string
Example
{
"claims": {
"email": {
"requirement": "OFF",
"state": "UNKNOWN"
},
"firstName": {
"requirement": "OFF",
"state": "UNKNOWN"
},
"lastName": {
"requirement": "OFF",
"state": "UNKNOWN"
}
}
}

Malformed request (e.g. invalid steamAppId).

Media type application/json

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
reason

Stable machine-readable reason code.

string
Example generated
{
"reason": "example"
}

Steam rejected the ticket as invalid.

Media type application/json

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
reason

Stable machine-readable reason code.

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

Media type application/json

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
reason
required

Stable machine-readable reason code.

string
claims

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
email
required

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
requirement
required

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.

string
Allowed values: OFF OPTIONAL REQUIRED SYNTHETIC
state
required
string
Allowed values: UNKNOWN GRANTED DENIED
firstName
required

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
requirement
required

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.

string
Allowed values: OFF OPTIONAL REQUIRED SYNTHETIC
state
required
string
Allowed values: UNKNOWN GRANTED DENIED
lastName
required

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
requirement
required

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.

string
Allowed values: OFF OPTIONAL REQUIRED SYNTHETIC
state
required
string
Allowed values: UNKNOWN GRANTED DENIED
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
errandKey
required

Bearer key (ernd_…); also the status-poll path key.

string
url
required

Open this in the user’s system browser.

string format: uri
expiresAt
required
string format: date-time
Example
{
"claims": {
"email": {
"requirement": "OFF",
"state": "UNKNOWN"
},
"firstName": {
"requirement": "OFF",
"state": "UNKNOWN"
},
"lastName": {
"requirement": "OFF",
"state": "UNKNOWN"
}
}
}

Application anchor not found.

Media type application/json

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
reason

Stable machine-readable reason code.

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

Media type application/json

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
reason

Stable machine-readable reason code.

string
Example generated
{
"reason": "example"
}

Steam’s verification endpoint was unreachable or returned an unparseable response.

Media type application/json

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
reason

Stable machine-readable reason code.

string
Example generated
{
"reason": "example"
}

Error response.

Media type application/json

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
reason

Stable machine-readable reason code.

string
Example generated
{
"reason": "example"
}