跳转到内容

Exchange an access-key credential for application tokens.

POST
/direct-issue/access-key
curl --request POST \
--url https://native-api.sudomimus.com/direct-issue/access-key \
--header 'Content-Type: application/json' \
--data '{ "applicationAnchor": "example", "accessKeyIdentifier": "example", "accessKeySecret": "example" }'

Trade a long-lived access-key credential (identifier + secret) for application access and refresh tokens. Access keys are application-scoped credentials bound to a specific account; the credential carries optional TTL bounds and an opt-in expiry timestamp.

The server:

  1. Validates the application’s authentication-rule layer admits ACCESS_KEY_DIRECT.
  2. Looks up the credential by accessKeyIdentifier.
  3. Cross-checks that the credential belongs to the application named in applicationAnchor, is not revoked, and has not expired.
  4. Timing-safe-verifies accessKeySecret.
  5. Validates the realize-rule layer (EMAIL / STEAM_ID / ACCOUNT_ALIAS / SECTOR_SUBJECT) and ensures a DIRECT_ISSUE return rule exists.
  6. Issues access + refresh JWTs and best-effort touches the credential’s lastUsedAt timestamp.

All credential-level failures (unknown identifier, app mismatch, revoked, expired, wrong secret) collapse into a single opaque AccessKeyDirectDenied 401 reason to avoid leaking identifier existence.

Media type application/json
object
applicationAnchor
required

Public anchor identifying the integrating application.

string
accessKeyIdentifier
required

UUID v4 string identifying the access-key credential. Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.

string
accessKeySecret
required

64-char lowercase hex string (32 random bytes) returned exactly once when the access key was issued. Never logged or persisted in plaintext server-side after creation.

string
Example generated
{
"applicationAnchor": "example",
"accessKeyIdentifier": "example",
"accessKeySecret": "example"
}

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 for renewal without re-presenting the access key.

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

Malformed request body. The reason distinguishes Invalid accessKeyIdentifier (must be a UUID v4) and Invalid accessKeySecret (must be 64 lowercase hex chars).

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 access-key credential was rejected. The reason is uniformly AccessKeyDirectDenied regardless of root cause (not found, app mismatch, revoked, expired, wrong secret).

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

Server error while resolving the credential’s account. Reason AccessKeyCredentialAccountMissing.

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