Session — API reference
The dashboard session: the credential a browser holds, and the only one it ever holds. An API key is a machine credential and is never used from a page.
Signing in is one action for the person: the page fetches a challenge
(POST /v1/accounts/challenge), the wallet signs it — free, no transaction, nothing
leaves the wallet — and the signature comes to POST /v1/session, which sets an
httpOnly cookie. The session survives reloads and navigation, slides forward as it is
used, and is restored silently with GET /v1/session.
Because the credential is a cookie, every unsafe method additionally needs the
X-CSRF-Token header, whose value is the csrf_token of the session response. A
cross-site page cannot read that response, so it cannot produce the header.
| Method | Path | Summary |
|---|---|---|
GET | /v1/session | Restore the session silently |
POST | /v1/session | Sign in to the dashboard |
DELETE | /v1/session | Sign out |
POST | /v1/session/refresh | Extend the session |
POST | /v1/session/revoke-all | Sign out everywhere |
POST | /v1/session/handoff | A single-use pass that opens the Nile cabinet without a second signature |
POST | /v1/session/handoff/accept | Sign in to the Nile cabinet with a mainnet pass |
POST | /v1/session/email/start | Send an e-mail sign-in code |
POST | /v1/session/email/verify | Sign in with an e-mail code |
GET | /v1/session/identities | The signed-in person’s ways in |
DELETE | /v1/session/identities/{id} | Unlink a way in |
POST | /v1/session/identities/email/start | Send a code to link an e-mail |
POST | /v1/session/identities/email/verify | Link an e-mail with its code |
POST | /v1/session/identities/wallet/challenge | The message that links a wallet |
POST | /v1/session/identities/wallet/verify | Link a wallet with its signature |
POST | /v1/session/identities/wallet/ticket | Link a wallet with the promo redeem’s wallet ticket |
POST | /v1/session/wallet-ticket | Sign in with the promo redeem’s wallet ticket |
POST | /v1/session/identities/telegram/start | Start linking Telegram |
POST | /v1/session/identities/google/start | Start linking Google |
GET | /v1/session/methods | Which sign-in methods this API offers |
GET | /v1/session/telegram/start | Sign in with Telegram |
GET | /v1/session/telegram/callback | Telegram’s redirect back |
GET | /v1/session/google/start | Sign in with Google |
GET | /v1/session/google/callback | Google’s redirect back |
GET | /v1/session/step-up | The 2-step channel |
PUT | /v1/session/step-up | Choose the 2-step channel |
POST | /v1/session/step-up/nile | Confirm a Nile action through the person’s mainnet channel |
POST | /v1/step-up/{id}/approve | Approve one 2-step confirmation |
GET | /v1/session/history | Recent sign-ins |
Base URL https://api.tenergy.me/v1, or https://api-nile.tenergy.me/v1 on Nile (Environments). Every request below is signed as in Authentication unless its Auth line says otherwise.
Restore the session silently
GET /v1/session · getSession
Auth: Dashboard session cookie + X-CSRF-Token.
What the dashboard calls on every load. Answers with the live session, or
1013 session_expired when the browser has none — which is not an error condition to
report to the person, only the signal to show the sign-in screen.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | 1013 session_expired — no session, or it expired or was signed out. |
500 | Something broke on our side. |
Response fields (Session)
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | yes | For the “Account ID (for support)” field. Nothing in a UI has to show it anywhere else — the account’s title is display_name, or the shortened owner_address. |
account_status | enum: unfunded, active, suspended, closed | yes | |
display_name | string | null | ||
owner_address | string | null | The wallet that owns this account; signing in with it is how it is recovered. | |
address | string | null | yes | The address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code). |
role | enum: owner, editor, viewer | null | yes | This signer’s role on the account. It, and nothing else, decides what they may do. |
network | enum: mainnet, nile | ||
csrf_token | string | yes | Echo in X-CSRF-Token on every POST, PATCH and DELETE of this session. |
expires_at | string (date-time) | yes | |
created_at | string (date-time) | ||
ttl_seconds | integer | The full session lifetime, in seconds, as renewal resets it. |
Sign in to the dashboard
POST /v1/session · createSession
Auth: Public — no credentials.
Verifies a challenge signed by a TRON address and opens a browser session, returned as
an httpOnly cookie. If the address owns no account on this brand yet, one is created
here — status: unfunded, the same account POST /v1/accounts would have made — so a
first visit and a return visit are the same single action.
Signing the challenge is free and is not a transaction: no funds move and no key leaves the wallet.
The response body carries csrf_token. Send it as X-CSRF-Token on every subsequent
POST, PATCH or DELETE made with this session.
Request body
JSON (SessionRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
address | string | yes | Base58Check TRON address (starts with T, 34 characters). |
nonce | string | yes | From POST /v1/accounts/challenge. Single use. |
signature | string | yes | The wallet’s signature over the challenge message. |
Responses
| Status | Meaning |
|---|---|
201 | Signed in. The session cookie is set. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
401 | 1010 challenge_invalid — unknown, expired or reused nonce, or a signature that does not recover the address. |
429 | Too many requests. |
500 | Something broke on our side. |
Response fields (Session)
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | yes | For the “Account ID (for support)” field. Nothing in a UI has to show it anywhere else — the account’s title is display_name, or the shortened owner_address. |
account_status | enum: unfunded, active, suspended, closed | yes | |
display_name | string | null | ||
owner_address | string | null | The wallet that owns this account; signing in with it is how it is recovered. | |
address | string | null | yes | The address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code). |
role | enum: owner, editor, viewer | null | yes | This signer’s role on the account. It, and nothing else, decides what they may do. |
network | enum: mainnet, nile | ||
csrf_token | string | yes | Echo in X-CSRF-Token on every POST, PATCH and DELETE of this session. |
expires_at | string (date-time) | yes | |
created_at | string (date-time) | ||
ttl_seconds | integer | The full session lifetime, in seconds, as renewal resets it. |
Sign out
DELETE /v1/session · deleteSession
Auth: Dashboard session cookie + X-CSRF-Token.
Revokes this session server-side and clears the cookie. Other sessions of the same account — another browser, another device — are untouched.
Responses
| Status | Meaning |
|---|---|
204 | Signed out. No body. |
401 | Missing, malformed or rejected credentials. |
403 | 1014 csrf_token_invalid — the X-CSRF-Token header is missing or wrong. |
500 | Something broke on our side. |
Extend the session
POST /v1/session/refresh · refreshSession
Auth: Dashboard session cookie + X-CSRF-Token.
Slides the expiry forward and reissues the cookie with a fresh lifetime. The session is also renewed automatically as it is used, so this is for a page that has been open a long time without making a request.
Responses
| Status | Meaning |
|---|---|
200 | Extended |
401 | Missing, malformed or rejected credentials. |
403 | 1014 csrf_token_invalid. |
500 | Something broke on our side. |
Response fields (Session)
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | yes | For the “Account ID (for support)” field. Nothing in a UI has to show it anywhere else — the account’s title is display_name, or the shortened owner_address. |
account_status | enum: unfunded, active, suspended, closed | yes | |
display_name | string | null | ||
owner_address | string | null | The wallet that owns this account; signing in with it is how it is recovered. | |
address | string | null | yes | The address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code). |
role | enum: owner, editor, viewer | null | yes | This signer’s role on the account. It, and nothing else, decides what they may do. |
network | enum: mainnet, nile | ||
csrf_token | string | yes | Echo in X-CSRF-Token on every POST, PATCH and DELETE of this session. |
expires_at | string (date-time) | yes | |
created_at | string (date-time) | ||
ttl_seconds | integer | The full session lifetime, in seconds, as renewal resets it. |
Sign out everywhere
POST /v1/session/revoke-all · revokeAllSessions
Auth: Dashboard session cookie + X-CSRF-Token.
Revokes every session of the signed-in wallet on this account — every browser and
device, this one included — and clears the cookie. Sessions of other members are not
touched. Writes an audit row (session.revoke_all). Dashboard session only, member
role viewer or above; every API key is refused.
Responses
| Status | Meaning |
|---|---|
204 | Signed out everywhere. No body. |
401 | Missing, malformed or rejected credentials. |
403 | 1014 csrf_token_invalid. |
500 | Something broke on our side. |
A single-use pass that opens the Nile cabinet without a second signature
POST /v1/session/handoff · createSessionHandoff
Auth: Dashboard session cookie + X-CSRF-Token.
Mainnet only, dashboard session only. Signs a pass for the signed-in person, whatever
method they signed in with: the person, the sign-in method used, the person’s verified wallets
and e-mails (no secrets) and the session’s expiry; audience nile, valid 60 seconds and once.
The Nile api accepts it at POST /session/handoff/accept. One way only: nothing the Nile api
issues opens a mainnet session. 404 on a stack without the handoff key, or for a session
with no sign-in method on record.
The token is <body>.<signature>: base64url JSON, Ed25519-signed. Pass v2 body:
{v: 2, typ: "session", iss: "mainnet", aud: "nile", jti, exp (60 s), brand, person: {id, step_up}, identity: {id, kind, display}, subjects: [{kind, subject}], session_exp}.
The same key signs two other types the site never asks for here: step_up (from
POST /session/step-up/nile) and event (mainnet to Nile: sign-out, a removed method); typ
is checked, so one type never serves as another.
Responses
| Status | Meaning |
|---|---|
200 | The pass. |
401 | Missing, malformed or rejected credentials. |
404 | Not offered on this stack. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | |
audience | enum: nile | yes | |
expires_at | string (date-time) | yes |
Sign in to the Nile cabinet with a mainnet pass
POST /v1/session/handoff/accept · acceptSessionHandoff
Auth: Public — no credentials.
Nile only. Checks the pass (Ed25519 signature by the mainnet stack, audience nile, type,
brand, not expired, not used before) and sets the session cookie for the person it names,
opening their own Nile account at the first pass. The Nile session ends no later than the
mainnet session it came from. 1003 when the pass is refused; 404 on a stack that does not
accept handoffs.
On a Nile stack that accepts passes this is the ONLY way in: POST /session,
/session/email/*, /session/telegram|google/start|callback and /session/identities* answer
404 there (the person signs in and manages sign-in methods on mainnet), and
GET /session/methods answers every method off with sign_in_on: mainnet.
Request body
JSON, required.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes |
Responses
| Status | Meaning |
|---|---|
201 | Signed in; the cookie is set. |
401 | Missing, malformed or rejected credentials. |
404 | Not accepted on this stack. |
Response fields (Session)
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | yes | For the “Account ID (for support)” field. Nothing in a UI has to show it anywhere else — the account’s title is display_name, or the shortened owner_address. |
account_status | enum: unfunded, active, suspended, closed | yes | |
display_name | string | null | ||
owner_address | string | null | The wallet that owns this account; signing in with it is how it is recovered. | |
address | string | null | yes | The address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code). |
role | enum: owner, editor, viewer | null | yes | This signer’s role on the account. It, and nothing else, decides what they may do. |
network | enum: mainnet, nile | ||
csrf_token | string | yes | Echo in X-CSRF-Token on every POST, PATCH and DELETE of this session. |
expires_at | string (date-time) | yes | |
created_at | string (date-time) | ||
ttl_seconds | integer | The full session lifetime, in seconds, as renewal resets it. |
Send an e-mail sign-in code
POST /v1/session/email/start · startEmailSignIn
Auth: Public — no credentials.
Sends a six-digit code, valid 10 minutes, to the e-mail. The answer is the same whether or
not the e-mail has an account; when a code was sent less than a minute ago (or too many in
the last hour) nothing is sent and retry_after says how long to wait. A new code kills the
previous one. Rate-limited per IP (1100). When GET /session/methods names email_captcha, a passed
Turnstile check’s captcha_token is required (403 1029 captcha_failed).
Request body
JSON (EmailCodeStartRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | yes | |
locale | string | ru... sends the Russian mail; anything else English. | |
captcha_token | string | The Turnstile token, required on POST /session/email/start when GET /session/methods names email_captcha (else 403 1029 captcha_failed). Single use. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
403 | Authenticated, but this key is not allowed to do it. |
429 | Too many requests. |
500 | Something broke on our side. |
503 | Temporarily unable to serve. On order creation this is fail-secure: nothing was stored and nothing was charged, so retrying verbatim with the same client_order_id is safe. |
Response fields (EmailCodeStarted)
| Field | Type | Required | Description |
|---|---|---|---|
expires_in | integer | yes | Seconds a code lives (600). |
resend_after | integer | yes | Seconds before another code may be requested (60). |
retry_after | integer | Present when no code was sent because one was sent too recently; wait this long. |
Sign in with an e-mail code
POST /v1/session/email/verify · verifyEmailSignIn
Auth: Public — no credentials.
Checks the code and opens a browser session exactly as POST /session does (cookie,
csrf_token). An e-mail with no account gets one here: no owner wallet, the e-mail as its
owner’s way in. address in the answer is null. 1016 login_code_invalid carries
details.attempts_left (five guesses per code); 1017 login_code_expired means request a
new code. Optional first_touch as on POST /session.
Request body
JSON, required.
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | yes | |
code | string | yes | |
first_touch | object |
Responses
| Status | Meaning |
|---|---|
201 | Signed in. The session cookie is set. |
400 | 1016 login_code_invalid, 1017 login_code_expired, or 2001 validation_failed. |
429 | Too many requests. |
500 | Something broke on our side. |
Response fields (Session)
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | yes | For the “Account ID (for support)” field. Nothing in a UI has to show it anywhere else — the account’s title is display_name, or the shortened owner_address. |
account_status | enum: unfunded, active, suspended, closed | yes | |
display_name | string | null | ||
owner_address | string | null | The wallet that owns this account; signing in with it is how it is recovered. | |
address | string | null | yes | The address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code). |
role | enum: owner, editor, viewer | null | yes | This signer’s role on the account. It, and nothing else, decides what they may do. |
network | enum: mainnet, nile | ||
csrf_token | string | yes | Echo in X-CSRF-Token on every POST, PATCH and DELETE of this session. |
expires_at | string (date-time) | yes | |
created_at | string (date-time) | ||
ttl_seconds | integer | The full session lifetime, in seconds, as renewal resets it. |
The signed-in person’s ways in
GET /v1/session/identities · listSessionIdentities
Auth: Dashboard session cookie + X-CSRF-Token.
Wallet, e-mail and later Telegram/Google identities of the signed-in member, oldest first. Dashboard session only.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Missing, malformed or rejected credentials. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
data | array of object (Identity) | yes | |
data[].id | string | yes | |
data[].kind | enum: wallet, email, telegram, google | yes | |
data[].display | string | null | yes | What to show - the address, or the e-mail as it was typed. |
data[].verified_at | string (date-time) | null | yes | |
data[].last_used_at | string (date-time) | null | yes | |
data[].created_at | string (date-time) | yes | |
data[].current | boolean | yes | The identity this session signed in with. |
data[].moved_from_empty_account | object | Only in a link answer, when the method was taken over from another, empty account (spec §1.4). | |
data[].moved_from_empty_account.account_closed | boolean | yes | That account had no way in left and is now closed. |
Unlink a way in
DELETE /v1/session/identities/{id} · deleteSessionIdentity
Auth: Dashboard session cookie + X-CSRF-Token.
Removes one identity of the signed-in member. 1019 last_identity when it is the only one.
With 2-step on, needs a confirmation (1020 step_up_required). Dashboard session only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
X-Step-Up | header | string | A 2-step approval: details.step_up_id of a 1020 step_up_required answer, approved with POST /v1/step-up/{id}/approve. Sent on the repeat of the identical request; used once. Only a dashboard session whose person switched 2-step on is ever asked for it. |
Responses
| Status | Meaning |
|---|---|
204 | Unlinked. |
401 | Missing, malformed or rejected credentials. |
403 | 1020 step_up_required (2-step on: approve, then repeat with X-Step-Up), 1021 step_up_invalid, 1014 csrf_token_invalid. |
404 | No such identity on this member. |
409 | 1019 last_identity. |
Send a code to link an e-mail
POST /v1/session/identities/email/start · startEmailLink
Auth: Dashboard session cookie + X-CSRF-Token.
As POST /session/email/start, for adding an e-mail to the signed-in member. With 2-step on,
needs a confirmation first (1020 step_up_required). Dashboard session only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Step-Up | header | string | A 2-step approval: details.step_up_id of a 1020 step_up_required answer, approved with POST /v1/step-up/{id}/approve. Sent on the repeat of the identical request; used once. Only a dashboard session whose person switched 2-step on is ever asked for it. |
Request body
JSON (EmailCodeStartRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | yes | |
locale | string | ru... sends the Russian mail; anything else English. | |
captcha_token | string | The Turnstile token, required on POST /session/email/start when GET /session/methods names email_captcha (else 403 1029 captcha_failed). Single use. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
401 | Missing, malformed or rejected credentials. |
403 | 1020 step_up_required (2-step on: approve, then repeat with X-Step-Up), 1021 step_up_invalid, 1014 csrf_token_invalid. |
429 | Too many requests. |
503 | Temporarily unable to serve. On order creation this is fail-secure: nothing was stored and nothing was charged, so retrying verbatim with the same client_order_id is safe. |
Response fields (EmailCodeStarted)
| Field | Type | Required | Description |
|---|---|---|---|
expires_in | integer | yes | Seconds a code lives (600). |
resend_after | integer | yes | Seconds before another code may be requested (60). |
retry_after | integer | Present when no code was sent because one was sent too recently; wait this long. |
Link an e-mail with its code
POST /v1/session/identities/email/verify · verifyEmailLink
Auth: Dashboard session cookie + X-CSRF-Token.
Checks the code and adds the e-mail as a way in for the signed-in member. An e-mail that signs in to
another, EMPTY account (no balance, hold, orders, withdrawals, live keys, webhooks, node keys,
subscriptions, OAuth grants, other members) moves here: the answer carries moved_from_empty_account
and that account is closed when it has no way in left. Another account that is not empty is
1023 identity_in_use_account_not_empty; another member of this account is 1018 identity_taken.
Dashboard session only.
Request body
JSON (EmailCodeVerifyRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | yes | |
code | string | yes |
Responses
| Status | Meaning |
|---|---|
201 | Linked. |
400 | 1016 login_code_invalid, 1017 login_code_expired, or 2001 validation_failed. |
401 | Missing, malformed or rejected credentials. |
409 | 1023 identity_in_use_account_not_empty (details.reason), 1018 identity_taken. |
Response fields (Identity)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | |
kind | enum: wallet, email, telegram, google | yes | |
display | string | null | yes | What to show - the address, or the e-mail as it was typed. |
verified_at | string (date-time) | null | yes | |
last_used_at | string (date-time) | null | yes | |
created_at | string (date-time) | yes | |
current | boolean | yes | The identity this session signed in with. |
moved_from_empty_account | object | Only in a link answer, when the method was taken over from another, empty account (spec §1.4). | |
moved_from_empty_account.account_closed | boolean | yes | That account had no way in left and is now closed. |
The message that links a wallet
POST /v1/session/identities/wallet/challenge · startWalletLink
Auth: Dashboard session cookie + X-CSRF-Token.
The TIP-191 challenge of the wallet sign-in, issued with purpose link, for adding a TRON wallet to
the signed-in member. When the wallet already reaches another account (its wallet sign-in, a member
of another account, or an account it created) the link goes ahead only if that account is empty, as
for verifyEmailLink; otherwise 1023 identity_in_use_account_not_empty. With 2-step on,
needs a confirmation first (1020 step_up_required). Dashboard session only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Step-Up | header | string | A 2-step approval: details.step_up_id of a 1020 step_up_required answer, approved with POST /v1/step-up/{id}/approve. Sent on the repeat of the identical request; used once. Only a dashboard session whose person switched 2-step on is ever asked for it. |
Request body
JSON, required.
| Field | Type | Required | Description |
|---|---|---|---|
address | string | yes | Base58Check TRON address (starts with T, 34 characters). |
Responses
| Status | Meaning |
|---|---|
201 | Issued; sign message in the wallet. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
401 | Missing, malformed or rejected credentials. |
403 | 1020 step_up_required (2-step on: approve, then repeat with X-Step-Up), 1021 step_up_invalid, 1014 csrf_token_invalid. |
409 | 1023 identity_in_use_account_not_empty (details.reason), 1018 identity_taken. |
Response fields (AccountChallenge)
| Field | Type | Required | Description |
|---|---|---|---|
nonce | string | yes | Single-use |
address | string | yes | Base58Check TRON address (starts with T, 34 characters). |
purpose | enum: signup, login, recovery, link, promo_redeem, promo_dev_code | ||
message | string | yes | The exact string to sign, TIP-191 personal-sign style. Domain-bound and human-readable, so that a person signing it in a wallet can see what it says. Sign these bytes verbatim — do not re-wrap, trim or re-encode them. |
issued_at | string (date-time) | yes | |
expires_at | string (date-time) | yes | Ten minutes after issue. An expired nonce gives 1010 challenge_invalid. |
Link a wallet with its signature
POST /v1/session/identities/wallet/verify · verifyWalletLink
Auth: Dashboard session cookie + X-CSRF-Token.
The signature over a link challenge adds the wallet as a way in for the signed-in member; a wallet
sign-in with it then opens this account. A wallet of another, empty account moves here (that account
loses the wallet as member address and owner_address, and is closed when no way in is left). A challenge issued for anything else, or a signature that
does not recover address, is 1010 challenge_invalid. Dashboard session only.
Request body
JSON (AccountChallengeVerifyRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
address | string | yes | Base58Check TRON address (starts with T, 34 characters). |
nonce | string | yes | |
signature | string | yes | Hex signature over message. Must recover address; 0x prefix optional. |
Responses
| Status | Meaning |
|---|---|
201 | Linked. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
401 | Missing, malformed or rejected credentials. |
409 | 1023 identity_in_use_account_not_empty (details.reason), 1018 identity_taken. |
Response fields (Identity)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | |
kind | enum: wallet, email, telegram, google | yes | |
display | string | null | yes | What to show - the address, or the e-mail as it was typed. |
verified_at | string (date-time) | null | yes | |
last_used_at | string (date-time) | null | yes | |
created_at | string (date-time) | yes | |
current | boolean | yes | The identity this session signed in with. |
moved_from_empty_account | object | Only in a link answer, when the method was taken over from another, empty account (spec §1.4). | |
moved_from_empty_account.account_closed | boolean | yes | That account had no way in left and is now closed. |
Link a wallet with the promo redeem’s wallet ticket
POST /v1/session/identities/wallet/ticket · linkWalletByTicket
Auth: Dashboard session cookie + X-CSRF-Token.
The same as POST /session/identities/wallet/verify, with the wallet_ticket from POST /promo/redeem
in place of a signature: the person signed once, on the redeem page. A ticket is single-use, lives ten
minutes and is spent even when the link is refused. Unknown, expired or spent: 1010 challenge_invalid.
Dashboard session only.
Request body
JSON (WalletTicketRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
ticket | string | yes | wallet_ticket.token from POST /promo/redeem. |
Responses
| Status | Meaning |
|---|---|
201 | Linked. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
401 | Missing, malformed or rejected credentials. |
409 | 1023 identity_in_use_account_not_empty (details.reason), 1018 identity_taken. |
Response fields (Identity)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | |
kind | enum: wallet, email, telegram, google | yes | |
display | string | null | yes | What to show - the address, or the e-mail as it was typed. |
verified_at | string (date-time) | null | yes | |
last_used_at | string (date-time) | null | yes | |
created_at | string (date-time) | yes | |
current | boolean | yes | The identity this session signed in with. |
moved_from_empty_account | object | Only in a link answer, when the method was taken over from another, empty account (spec §1.4). | |
moved_from_empty_account.account_closed | boolean | yes | That account had no way in left and is now closed. |
Sign in with the promo redeem’s wallet ticket
POST /v1/session/wallet-ticket · createSessionByWalletTicket
Auth: Public — no credentials.
A wallet sign-in (POST /session) with the wallet_ticket from POST /promo/redeem in place of a
signature: the wallet’s own account, or a new wallet account for a first-time wallet. Single-use, ten
minutes. Unknown, expired or spent: 1010 challenge_invalid.
Request body
JSON (WalletTicketRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
ticket | string | yes | wallet_ticket.token from POST /promo/redeem. |
Responses
| Status | Meaning |
|---|---|
201 | Signed in; the session cookie is set. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
401 | Missing, malformed or rejected credentials. |
429 | Too many requests. |
Response fields (Session)
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | yes | For the “Account ID (for support)” field. Nothing in a UI has to show it anywhere else — the account’s title is display_name, or the shortened owner_address. |
account_status | enum: unfunded, active, suspended, closed | yes | |
display_name | string | null | ||
owner_address | string | null | The wallet that owns this account; signing in with it is how it is recovered. | |
address | string | null | yes | The address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code). |
role | enum: owner, editor, viewer | null | yes | This signer’s role on the account. It, and nothing else, decides what they may do. |
network | enum: mainnet, nile | ||
csrf_token | string | yes | Echo in X-CSRF-Token on every POST, PATCH and DELETE of this session. |
expires_at | string (date-time) | yes | |
created_at | string (date-time) | ||
ttl_seconds | integer | The full session lifetime, in seconds, as renewal resets it. |
Start linking Telegram
POST /v1/session/identities/telegram/start · startTelegramLink
Auth: Dashboard session cookie + X-CSRF-Token.
Answers the Telegram URL to open. Telegram returns the browser to return_to with ?linked=telegram,
with &moved=closed (or &moved=kept) when it was taken over from another, empty account, or
?signin_error=identity_in_use_account_not_empty|identity_taken|telegram; the link needs this
browser’s session at the callback.
404 while Telegram sign-in is off. With 2-step on, needs a confirmation first. Dashboard session only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Step-Up | header | string | A 2-step approval: details.step_up_id of a 1020 step_up_required answer, approved with POST /v1/step-up/{id}/approve. Sent on the repeat of the identical request; used once. Only a dashboard session whose person switched 2-step on is ever asked for it. |
Request body
JSON (OidcLinkRequest).
| Field | Type | Required | Description |
|---|---|---|---|
return_to | string | Cabinet path to come back to (/app… or /xx/app…, Nile /app/nile… included); default the profile. |
Responses
| Status | Meaning |
|---|---|
200 | Where to send the browser. |
401 | Missing, malformed or rejected credentials. |
403 | 1020 step_up_required, 1021 step_up_invalid, 1014 csrf_token_invalid. |
404 | No such object, or it belongs to another account. The two are not distinguished. |
Response fields (OidcLinkStarted)
| Field | Type | Required | Description |
|---|---|---|---|
url | string (uri) | yes | The provider’s authorization URL to open. |
Start linking Google
POST /v1/session/identities/google/start · startGoogleLink
Auth: Dashboard session cookie + X-CSRF-Token.
As startTelegramLink, for a Google account; back with ?linked=google or
?signin_error=identity_in_use_account_not_empty|identity_taken|google. 404 while Google sign-in is off. Dashboard session only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Step-Up | header | string | A 2-step approval: details.step_up_id of a 1020 step_up_required answer, approved with POST /v1/step-up/{id}/approve. Sent on the repeat of the identical request; used once. Only a dashboard session whose person switched 2-step on is ever asked for it. |
Request body
JSON (OidcLinkRequest).
| Field | Type | Required | Description |
|---|---|---|---|
return_to | string | Cabinet path to come back to (/app… or /xx/app…, Nile /app/nile… included); default the profile. |
Responses
| Status | Meaning |
|---|---|
200 | Where to send the browser. |
401 | Missing, malformed or rejected credentials. |
403 | 1020 step_up_required, 1021 step_up_invalid, 1014 csrf_token_invalid. |
404 | No such object, or it belongs to another account. The two are not distinguished. |
Response fields (OidcLinkStarted)
| Field | Type | Required | Description |
|---|---|---|---|
url | string (uri) | yes | The provider’s authorization URL to open. |
Which sign-in methods this API offers
GET /v1/session/methods · getSignInMethods
Auth: Public — no credentials.
Telegram and Google are each off until their OAuth client is configured. Anonymous. On a Nile
stack that takes mainnet passes every method is off and sign_in_on: mainnet: sign in on the
mainnet API, then hand the session over (POST /session/handoff).
Responses
| Status | Meaning |
|---|---|
200 | The methods. |
Response fields (SignInMethods)
| Field | Type | Required | Description |
|---|---|---|---|
wallet_login | boolean | yes | |
email_login | boolean | yes | |
telegram_login | boolean | yes | |
google_login | boolean | yes | |
sign_in_on | enum: mainnet | Nile only: people sign in on the mainnet API and arrive here by pass. | |
email_captcha | object | Present when POST /session/email/start needs a bot check: render the Cloudflare Turnstile widget with this site key and action, and send its token as captcha_token (one token per code request). | |
email_captcha.provider | enum: turnstile | yes | |
email_captcha.site_key | string | yes | |
email_captcha.action | string | yes |
Sign in with Telegram
GET /v1/session/telegram/start · startTelegramSignIn
Auth: Public — no credentials.
A browser navigation: sets a signed, HttpOnly flow cookie (state, nonce, PKCE verifier) and answers 302 to Telegram’s OpenID Connect authorization endpoint. 404 while Telegram sign-in is off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
return_to | query | string | A path on our site to come back to: the cabinet (/app… or /xx/app…, Nile /app/nile… included), or a Nile consent / claim page (/oauth/consent?… or /agent/claim?…, optional /xx prefix) whose query carries exactly one network=nile. Nothing with //, a backslash, a fragment or other characters; anything else falls back to the cabinet. | |
first_touch | query | string | How the visitor first found the site (JSON, at most 2000 characters); stored once on a new account. |
Responses
| Status | Meaning |
|---|---|
302 | To Telegram. |
404 | No such object, or it belongs to another account. The two are not distinguished. |
Telegram’s redirect back
GET /v1/session/telegram/callback · telegramSignInCallback
Auth: Public — no credentials.
Exchanges the code (PKCE), verifies the ID token (RS256 against Telegram’s JWKS; iss, aud, exp,
nonce) and signs in — a new Telegram account opens a new account — or links it, then 302 to the
site path the flow started from (?signin_error=telegram|identity_taken on failure).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
code | query | string | ||
state | query | string | ||
error | query | string |
Responses
| Status | Meaning |
|---|---|
302 | Back to the site. |
404 | No such object, or it belongs to another account. The two are not distinguished. |
Sign in with Google
GET /v1/session/google/start · startGoogleSignIn
Auth: Public — no credentials.
As startTelegramSignIn, to Google (scope openid email profile, PKCE S256, nonce). 404 while Google
sign-in is off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
return_to | query | string | A path on our site to come back to: the cabinet (/app… or /xx/app…, Nile /app/nile… included), or a Nile consent / claim page (/oauth/consent?… or /agent/claim?…, optional /xx prefix) whose query carries exactly one network=nile. Nothing with //, a backslash, a fragment or other characters; anything else falls back to the cabinet. | |
first_touch | query | string | How the visitor first found the site (JSON, at most 2000 characters); stored once on a new account. |
Responses
| Status | Meaning |
|---|---|
302 | To Google. |
404 | No such object, or it belongs to another account. The two are not distinguished. |
Google’s redirect back
GET /v1/session/google/callback · googleSignInCallback
Auth: Public — no credentials.
As telegramSignInCallback; the ID token is checked against Google’s JWKS with iss
https://accounts.google.com or accounts.google.com. The identity is the Google sub: a Google
account whose e-mail equals an e-mail sign-in here is not joined to it — signing in opens a new
account; link it from inside instead.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
code | query | string | ||
state | query | string | ||
error | query | string |
Responses
| Status | Meaning |
|---|---|
302 | Back to the site. |
404 | No such object, or it belongs to another account. The two are not distinguished. |
The 2-step channel
GET /v1/session/step-up · getStepUp
Auth: Dashboard session cookie + X-CSRF-Token.
Which second channel confirms risky actions of the signed-in person (withdrawals, API key
creation, linking and unlinking sign-in methods, changing this setting) and which channels
they can choose now. off until the person switches it on. Dashboard session only.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Missing, malformed or rejected credentials. |
Response fields (StepUpState)
| Field | Type | Required | Description |
|---|---|---|---|
channel | enum: off, email, telegram, wallet | yes | |
available | array of enum: email, telegram, wallet | yes | Channels this person can switch to now. |
email | object (StepUpEmail) | null | yes | Where e-mail codes go: the person’s choice while it is usable, else the first usable address (an e-mail method before a Google address). Null when there is none. |
email.identity_id | string | yes | The sign-in method the address comes from. |
email.address | string | yes | The whole address: it is the signed-in person’s own. |
email.source | enum: email, google | yes | email: a verified e-mail sign-in method; google: the address a linked Google account reports as verified. A Google address only receives codes; it never signs in. |
emails | array of object (StepUpEmail) | yes | Every address that can receive codes, in fallback order. |
emails[].identity_id | string | yes | The sign-in method the address comes from. |
emails[].address | string | yes | The whole address: it is the signed-in person’s own. |
emails[].source | enum: email, google | yes | email: a verified e-mail sign-in method; google: the address a linked Google account reports as verified. A Google address only receives codes; it never signs in. |
telegram | object | null | yes | The Telegram account the bot sends codes to; null without one or without the bot. |
telegram.identity_id | string | yes | |
telegram.display | string | null | yes | The account’s @username or name, when Telegram gave one. |
google_email_unknown | boolean | yes | Always false; kept for older clients. A Google account linked before its address was recorded offers the address it was linked with. |
Choose the 2-step channel
PUT /v1/session/step-up · setStepUp
Auth: Dashboard session cookie + X-CSRF-Token.
email needs an address that can receive codes (a verified e-mail sign-in method, or a Google
account whose address Google verified), wallet a linked wallet, telegram a linked Telegram account
with the bot configured on this deployment (1022 step_up_channel_unavailable otherwise).
identity_id (e-mail only) picks the address among emails. Switching on from off needs no
confirmation: every channel was proven at sign-in. Switching off, to another channel or to another
address is confirmed once, through the CURRENT channel: 1020 step_up_required, approved and repeated
with X-Step-Up. A code that went to the same place under 15 s ago and is still unused answers
1100 rate_limited with details.scope: step_up and details.retry_after. Choosing telegram, the bot
writes that confirmation is on; when it cannot write to the person (Start never pressed, or the bot
blocked) the answer is 1025 telegram_bot_not_started with details.bot_username and nothing changes; a Telegram account linked before the bot could be granted access answers 1026 telegram_relink_needed (link it again).
On Nile, a session opened by a mainnet pass cannot change it: 1022 with details.reason: set_on_mainnet (the channel is the person’s, chosen on mainnet, and confirms Nile actions too).
Dashboard session only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Step-Up | header | string | A 2-step approval: details.step_up_id of a 1020 step_up_required answer, approved with POST /v1/step-up/{id}/approve. Sent on the repeat of the identical request; used once. Only a dashboard session whose person switched 2-step on is ever asked for it. |
Request body
JSON, required.
| Field | Type | Required | Description |
|---|---|---|---|
channel | enum: off, email, telegram, wallet | yes | |
identity_id | string | E-mail only: the emails[].identity_id codes go to. |
Responses
| Status | Meaning |
|---|---|
200 | Saved. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
401 | Missing, malformed or rejected credentials. |
403 | 1020 step_up_required (2-step on: approve, then repeat with X-Step-Up), 1021 step_up_invalid, 1014 csrf_token_invalid. |
409 | 1022 step_up_channel_unavailable, 1025 telegram_bot_not_started, 1026 telegram_relink_needed. |
Response fields (StepUpState)
| Field | Type | Required | Description |
|---|---|---|---|
channel | enum: off, email, telegram, wallet | yes | |
available | array of enum: email, telegram, wallet | yes | Channels this person can switch to now. |
email | object (StepUpEmail) | null | yes | Where e-mail codes go: the person’s choice while it is usable, else the first usable address (an e-mail method before a Google address). Null when there is none. |
email.identity_id | string | yes | The sign-in method the address comes from. |
email.address | string | yes | The whole address: it is the signed-in person’s own. |
email.source | enum: email, google | yes | email: a verified e-mail sign-in method; google: the address a linked Google account reports as verified. A Google address only receives codes; it never signs in. |
emails | array of object (StepUpEmail) | yes | Every address that can receive codes, in fallback order. |
emails[].identity_id | string | yes | The sign-in method the address comes from. |
emails[].address | string | yes | The whole address: it is the signed-in person’s own. |
emails[].source | enum: email, google | yes | email: a verified e-mail sign-in method; google: the address a linked Google account reports as verified. A Google address only receives codes; it never signs in. |
telegram | object | null | yes | The Telegram account the bot sends codes to; null without one or without the bot. |
telegram.identity_id | string | yes | |
telegram.display | string | null | yes | The account’s @username or name, when Telegram gave one. |
google_email_unknown | boolean | yes | Always false; kept for older clients. A Google account linked before its address was recorded offers the address it was linked with. |
Confirm a Nile action through the person’s mainnet channel
POST /v1/session/step-up/nile · confirmNileStepUp
Auth: Dashboard session cookie + X-CSRF-Token.
Mainnet only, dashboard session only. A Nile session opened by a mainnet pass answers a
protected action with 1020 step_up_required and details.via: mainnet, step_up_id,
payload_hash, action_label. The page sends those here: the first call answers 1020 with a
MAINNET request through the person’s own channel (or the choose-channel answer when none is set);
approved and repeated with X-Step-Up, it answers a 60 s single-use step-up pass bound to that
Nile request id and payload hash. The page then approves the Nile request with
POST /step-up/{id}/approve {pass} on Nile and repeats the action there. 404 on a stack
without the handoff key.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Step-Up | header | string | A 2-step approval: details.step_up_id of a 1020 step_up_required answer, approved with POST /v1/step-up/{id}/approve. Sent on the repeat of the identical request; used once. Only a dashboard session whose person switched 2-step on is ever asked for it. |
Request body
JSON, required.
| Field | Type | Required | Description |
|---|---|---|---|
request_id | string | yes | details.step_up_id of the Nile 1020. |
payload_hash | string | yes | details.payload_hash of the Nile 1020. |
action_label | string | What the Nile action is; shown to the person only. |
Responses
| Status | Meaning |
|---|---|
200 | The step-up pass. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
401 | Missing, malformed or rejected credentials. |
403 | 1020 step_up_required (approve on mainnet, then repeat with X-Step-Up), 1021 step_up_invalid, 1014 csrf_token_invalid. |
404 | Not offered on this stack. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
pass | string | yes | |
expires_at | string (date-time) | yes |
Approve one 2-step confirmation
POST /v1/step-up/{id}/approve · approveStepUp
Auth: Dashboard session cookie + X-CSRF-Token.
id is details.step_up_id of a 1020 step_up_required answer. E-mail and Telegram channels: the
six-digit code from the mail or the bot’s message. Wallet channel: signature, TIP-191 over
details.message, from a wallet linked to this person. The approval is valid until
expires_at (5 minutes from the request) for one repeat of the identical request with
X-Step-Up: <id>. Dashboard session only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Request body
JSON (StepUpApproveRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
code | string | E-mail channel. | |
signature | string | Wallet channel: TIP-191 signature over details.message. | |
pass | string | Nile, a session opened by a mainnet pass (1020 with details.via: mainnet): the step-up pass from mainnet POST /session/step-up/nile for this request; codes and signatures do not apply. |
Responses
| Status | Meaning |
|---|---|
200 | Approved. |
400 | 1016 login_code_invalid, 1017 login_code_expired, or 2001 validation_failed. |
401 | Missing, malformed or rejected credentials. |
403 | 1021 step_up_invalid: unknown, not pending, expired, or a signature from no linked wallet. |
Response fields (StepUpApproved)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | |
status | enum: approved | yes | |
expires_at | string (date-time) | yes |
Recent sign-ins
GET /v1/session/history · getSessionHistory
Auth: Dashboard session cookie + X-CSRF-Token.
The last ten dashboard sign-ins of the signed-in wallet on this account, newest
first: when, from which IP, with which wallet. A sign-in is recorded when
POST /v1/session issues a session; the history starts on 2026-09-25.
Sign-ins of other members are not listed: showing one member’s IP addresses to another
is an access decision that has not been taken. Dashboard session only, member role
viewer or above.
Responses
| Status | Meaning |
|---|---|
200 | OK |
401 | Missing, malformed or rejected credentials. |
500 | Something broke on our side. |
Response fields
| Field | Type | Required | Description |
|---|---|---|---|
data | array of object (SignIn) | yes | |
data[].signed_in_at | string (date-time) | yes | |
data[].ip | string | null | yes | |
data[].address | string | null | yes | The wallet that signed in; null for a sign-in without a wallet (e-mail code). |
data[].method | string | null | How the person signed in: the kind of the identity used (wallet, email, telegram, google; more may follow, so show an unknown value generically). null when it is not known. |
Example 200 response, from the contract (illustrative values — live numbers come from the API):
{
"data": [
{
"signed_in_at": "2026-09-25T09:41:12.000Z",
"ip": "203.0.113.10",
"address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE"
}
]
}