Docs menu

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.

MethodPathSummary
GET/v1/sessionRestore the session silently
POST/v1/sessionSign in to the dashboard
DELETE/v1/sessionSign out
POST/v1/session/refreshExtend the session
POST/v1/session/revoke-allSign out everywhere
POST/v1/session/handoffA single-use pass that opens the Nile cabinet without a second signature
POST/v1/session/handoff/acceptSign in to the Nile cabinet with a mainnet pass
POST/v1/session/email/startSend an e-mail sign-in code
POST/v1/session/email/verifySign in with an e-mail code
GET/v1/session/identitiesThe signed-in person’s ways in
DELETE/v1/session/identities/{id}Unlink a way in
POST/v1/session/identities/email/startSend a code to link an e-mail
POST/v1/session/identities/email/verifyLink an e-mail with its code
POST/v1/session/identities/wallet/challengeThe message that links a wallet
POST/v1/session/identities/wallet/verifyLink a wallet with its signature
POST/v1/session/identities/wallet/ticketLink a wallet with the promo redeem’s wallet ticket
POST/v1/session/wallet-ticketSign in with the promo redeem’s wallet ticket
POST/v1/session/identities/telegram/startStart linking Telegram
POST/v1/session/identities/google/startStart linking Google
GET/v1/session/methodsWhich sign-in methods this API offers
GET/v1/session/telegram/startSign in with Telegram
GET/v1/session/telegram/callbackTelegram’s redirect back
GET/v1/session/google/startSign in with Google
GET/v1/session/google/callbackGoogle’s redirect back
GET/v1/session/step-upThe 2-step channel
PUT/v1/session/step-upChoose the 2-step channel
POST/v1/session/step-up/nileConfirm a Nile action through the person’s mainnet channel
POST/v1/step-up/{id}/approveApprove one 2-step confirmation
GET/v1/session/historyRecent 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

StatusMeaning
200OK
4011013 session_expired — no session, or it expired or was signed out.
500Something broke on our side.

Response fields (Session)

FieldTypeRequiredDescription
account_idstringyesFor 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_statusenum: unfunded, active, suspended, closedyes
display_namestring | null
owner_addressstring | nullThe wallet that owns this account; signing in with it is how it is recovered.
addressstring | nullyesThe address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code).
roleenum: owner, editor, viewer | nullyesThis signer’s role on the account. It, and nothing else, decides what they may do.
networkenum: mainnet, nile
csrf_tokenstringyesEcho in X-CSRF-Token on every POST, PATCH and DELETE of this session.
expires_atstring (date-time)yes
created_atstring (date-time)
ttl_secondsintegerThe 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.

FieldTypeRequiredDescription
addressstringyesBase58Check TRON address (starts with T, 34 characters).
noncestringyesFrom POST /v1/accounts/challenge. Single use.
signaturestringyesThe wallet’s signature over the challenge message.

Responses

StatusMeaning
201Signed in. The session cookie is set.
400Malformed request — bad JSON, unknown field, wrong type.
4011010 challenge_invalid — unknown, expired or reused nonce, or a signature that does not recover the address.
429Too many requests.
500Something broke on our side.

Response fields (Session)

FieldTypeRequiredDescription
account_idstringyesFor 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_statusenum: unfunded, active, suspended, closedyes
display_namestring | null
owner_addressstring | nullThe wallet that owns this account; signing in with it is how it is recovered.
addressstring | nullyesThe address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code).
roleenum: owner, editor, viewer | nullyesThis signer’s role on the account. It, and nothing else, decides what they may do.
networkenum: mainnet, nile
csrf_tokenstringyesEcho in X-CSRF-Token on every POST, PATCH and DELETE of this session.
expires_atstring (date-time)yes
created_atstring (date-time)
ttl_secondsintegerThe 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

StatusMeaning
204Signed out. No body.
401Missing, malformed or rejected credentials.
4031014 csrf_token_invalid — the X-CSRF-Token header is missing or wrong.
500Something 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

StatusMeaning
200Extended
401Missing, malformed or rejected credentials.
4031014 csrf_token_invalid.
500Something broke on our side.

Response fields (Session)

FieldTypeRequiredDescription
account_idstringyesFor 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_statusenum: unfunded, active, suspended, closedyes
display_namestring | null
owner_addressstring | nullThe wallet that owns this account; signing in with it is how it is recovered.
addressstring | nullyesThe address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code).
roleenum: owner, editor, viewer | nullyesThis signer’s role on the account. It, and nothing else, decides what they may do.
networkenum: mainnet, nile
csrf_tokenstringyesEcho in X-CSRF-Token on every POST, PATCH and DELETE of this session.
expires_atstring (date-time)yes
created_atstring (date-time)
ttl_secondsintegerThe 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

StatusMeaning
204Signed out everywhere. No body.
401Missing, malformed or rejected credentials.
4031014 csrf_token_invalid.
500Something 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

StatusMeaning
200The pass.
401Missing, malformed or rejected credentials.
404Not offered on this stack.

Response fields

FieldTypeRequiredDescription
tokenstringyes
audienceenum: nileyes
expires_atstring (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.

FieldTypeRequiredDescription
tokenstringyes

Responses

StatusMeaning
201Signed in; the cookie is set.
401Missing, malformed or rejected credentials.
404Not accepted on this stack.

Response fields (Session)

FieldTypeRequiredDescription
account_idstringyesFor 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_statusenum: unfunded, active, suspended, closedyes
display_namestring | null
owner_addressstring | nullThe wallet that owns this account; signing in with it is how it is recovered.
addressstring | nullyesThe address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code).
roleenum: owner, editor, viewer | nullyesThis signer’s role on the account. It, and nothing else, decides what they may do.
networkenum: mainnet, nile
csrf_tokenstringyesEcho in X-CSRF-Token on every POST, PATCH and DELETE of this session.
expires_atstring (date-time)yes
created_atstring (date-time)
ttl_secondsintegerThe 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.

FieldTypeRequiredDescription
emailstring (email)yes
localestringru... sends the Russian mail; anything else English.
captcha_tokenstringThe Turnstile token, required on POST /session/email/start when GET /session/methods names email_captcha (else 403 1029 captcha_failed). Single use.

Responses

StatusMeaning
202Accepted.
400Malformed request — bad JSON, unknown field, wrong type.
403Authenticated, but this key is not allowed to do it.
429Too many requests.
500Something broke on our side.
503Temporarily 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)

FieldTypeRequiredDescription
expires_inintegeryesSeconds a code lives (600).
resend_afterintegeryesSeconds before another code may be requested (60).
retry_afterintegerPresent 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.

FieldTypeRequiredDescription
emailstring (email)yes
codestringyes
first_touchobject

Responses

StatusMeaning
201Signed in. The session cookie is set.
4001016 login_code_invalid, 1017 login_code_expired, or 2001 validation_failed.
429Too many requests.
500Something broke on our side.

Response fields (Session)

FieldTypeRequiredDescription
account_idstringyesFor 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_statusenum: unfunded, active, suspended, closedyes
display_namestring | null
owner_addressstring | nullThe wallet that owns this account; signing in with it is how it is recovered.
addressstring | nullyesThe address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code).
roleenum: owner, editor, viewer | nullyesThis signer’s role on the account. It, and nothing else, decides what they may do.
networkenum: mainnet, nile
csrf_tokenstringyesEcho in X-CSRF-Token on every POST, PATCH and DELETE of this session.
expires_atstring (date-time)yes
created_atstring (date-time)
ttl_secondsintegerThe 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

StatusMeaning
200OK
401Missing, malformed or rejected credentials.

Response fields

FieldTypeRequiredDescription
dataarray of object (Identity)yes
data[].idstringyes
data[].kindenum: wallet, email, telegram, googleyes
data[].displaystring | nullyesWhat to show - the address, or the e-mail as it was typed.
data[].verified_atstring (date-time) | nullyes
data[].last_used_atstring (date-time) | nullyes
data[].created_atstring (date-time)yes
data[].currentbooleanyesThe identity this session signed in with.
data[].moved_from_empty_accountobjectOnly in a link answer, when the method was taken over from another, empty account (spec §1.4).
data[].moved_from_empty_account.account_closedbooleanyesThat account had no way in left and is now closed.

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

NameInTypeRequiredDescription
idpathstringyes
X-Step-UpheaderstringA 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

StatusMeaning
204Unlinked.
401Missing, malformed or rejected credentials.
4031020 step_up_required (2-step on: approve, then repeat with X-Step-Up), 1021 step_up_invalid, 1014 csrf_token_invalid.
404No such identity on this member.
4091019 last_identity.

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

NameInTypeRequiredDescription
X-Step-UpheaderstringA 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.

FieldTypeRequiredDescription
emailstring (email)yes
localestringru... sends the Russian mail; anything else English.
captcha_tokenstringThe Turnstile token, required on POST /session/email/start when GET /session/methods names email_captcha (else 403 1029 captcha_failed). Single use.

Responses

StatusMeaning
202Accepted.
400Malformed request — bad JSON, unknown field, wrong type.
401Missing, malformed or rejected credentials.
4031020 step_up_required (2-step on: approve, then repeat with X-Step-Up), 1021 step_up_invalid, 1014 csrf_token_invalid.
429Too many requests.
503Temporarily 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)

FieldTypeRequiredDescription
expires_inintegeryesSeconds a code lives (600).
resend_afterintegeryesSeconds before another code may be requested (60).
retry_afterintegerPresent when no code was sent because one was sent too recently; wait this long.

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.

FieldTypeRequiredDescription
emailstring (email)yes
codestringyes

Responses

StatusMeaning
201Linked.
4001016 login_code_invalid, 1017 login_code_expired, or 2001 validation_failed.
401Missing, malformed or rejected credentials.
4091023 identity_in_use_account_not_empty (details.reason), 1018 identity_taken.

Response fields (Identity)

FieldTypeRequiredDescription
idstringyes
kindenum: wallet, email, telegram, googleyes
displaystring | nullyesWhat to show - the address, or the e-mail as it was typed.
verified_atstring (date-time) | nullyes
last_used_atstring (date-time) | nullyes
created_atstring (date-time)yes
currentbooleanyesThe identity this session signed in with.
moved_from_empty_accountobjectOnly in a link answer, when the method was taken over from another, empty account (spec §1.4).
moved_from_empty_account.account_closedbooleanyesThat account had no way in left and is now closed.

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

NameInTypeRequiredDescription
X-Step-UpheaderstringA 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.

FieldTypeRequiredDescription
addressstringyesBase58Check TRON address (starts with T, 34 characters).

Responses

StatusMeaning
201Issued; sign message in the wallet.
400Malformed request — bad JSON, unknown field, wrong type.
401Missing, malformed or rejected credentials.
4031020 step_up_required (2-step on: approve, then repeat with X-Step-Up), 1021 step_up_invalid, 1014 csrf_token_invalid.
4091023 identity_in_use_account_not_empty (details.reason), 1018 identity_taken.

Response fields (AccountChallenge)

FieldTypeRequiredDescription
noncestringyesSingle-use
addressstringyesBase58Check TRON address (starts with T, 34 characters).
purposeenum: signup, login, recovery, link, promo_redeem, promo_dev_code
messagestringyesThe 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_atstring (date-time)yes
expires_atstring (date-time)yesTen minutes after issue. An expired nonce gives 1010 challenge_invalid.

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.

FieldTypeRequiredDescription
addressstringyesBase58Check TRON address (starts with T, 34 characters).
noncestringyes
signaturestringyesHex signature over message. Must recover address; 0x prefix optional.

Responses

StatusMeaning
201Linked.
400Malformed request — bad JSON, unknown field, wrong type.
401Missing, malformed or rejected credentials.
4091023 identity_in_use_account_not_empty (details.reason), 1018 identity_taken.

Response fields (Identity)

FieldTypeRequiredDescription
idstringyes
kindenum: wallet, email, telegram, googleyes
displaystring | nullyesWhat to show - the address, or the e-mail as it was typed.
verified_atstring (date-time) | nullyes
last_used_atstring (date-time) | nullyes
created_atstring (date-time)yes
currentbooleanyesThe identity this session signed in with.
moved_from_empty_accountobjectOnly in a link answer, when the method was taken over from another, empty account (spec §1.4).
moved_from_empty_account.account_closedbooleanyesThat account had no way in left and is now closed.

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.

FieldTypeRequiredDescription
ticketstringyeswallet_ticket.token from POST /promo/redeem.

Responses

StatusMeaning
201Linked.
400Malformed request — bad JSON, unknown field, wrong type.
401Missing, malformed or rejected credentials.
4091023 identity_in_use_account_not_empty (details.reason), 1018 identity_taken.

Response fields (Identity)

FieldTypeRequiredDescription
idstringyes
kindenum: wallet, email, telegram, googleyes
displaystring | nullyesWhat to show - the address, or the e-mail as it was typed.
verified_atstring (date-time) | nullyes
last_used_atstring (date-time) | nullyes
created_atstring (date-time)yes
currentbooleanyesThe identity this session signed in with.
moved_from_empty_accountobjectOnly in a link answer, when the method was taken over from another, empty account (spec §1.4).
moved_from_empty_account.account_closedbooleanyesThat 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.

FieldTypeRequiredDescription
ticketstringyeswallet_ticket.token from POST /promo/redeem.

Responses

StatusMeaning
201Signed in; the session cookie is set.
400Malformed request — bad JSON, unknown field, wrong type.
401Missing, malformed or rejected credentials.
429Too many requests.

Response fields (Session)

FieldTypeRequiredDescription
account_idstringyesFor 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_statusenum: unfunded, active, suspended, closedyes
display_namestring | null
owner_addressstring | nullThe wallet that owns this account; signing in with it is how it is recovered.
addressstring | nullyesThe address that signed in. The owner’s, unless a member was invited. null for a session signed in without a wallet (e-mail code).
roleenum: owner, editor, viewer | nullyesThis signer’s role on the account. It, and nothing else, decides what they may do.
networkenum: mainnet, nile
csrf_tokenstringyesEcho in X-CSRF-Token on every POST, PATCH and DELETE of this session.
expires_atstring (date-time)yes
created_atstring (date-time)
ttl_secondsintegerThe 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

NameInTypeRequiredDescription
X-Step-UpheaderstringA 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).

FieldTypeRequiredDescription
return_tostringCabinet path to come back to (/app… or /xx/app…, Nile /app/nile… included); default the profile.

Responses

StatusMeaning
200Where to send the browser.
401Missing, malformed or rejected credentials.
4031020 step_up_required, 1021 step_up_invalid, 1014 csrf_token_invalid.
404No such object, or it belongs to another account. The two are not distinguished.

Response fields (OidcLinkStarted)

FieldTypeRequiredDescription
urlstring (uri)yesThe 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

NameInTypeRequiredDescription
X-Step-UpheaderstringA 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).

FieldTypeRequiredDescription
return_tostringCabinet path to come back to (/app… or /xx/app…, Nile /app/nile… included); default the profile.

Responses

StatusMeaning
200Where to send the browser.
401Missing, malformed or rejected credentials.
4031020 step_up_required, 1021 step_up_invalid, 1014 csrf_token_invalid.
404No such object, or it belongs to another account. The two are not distinguished.

Response fields (OidcLinkStarted)

FieldTypeRequiredDescription
urlstring (uri)yesThe 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

StatusMeaning
200The methods.

Response fields (SignInMethods)

FieldTypeRequiredDescription
wallet_loginbooleanyes
email_loginbooleanyes
telegram_loginbooleanyes
google_loginbooleanyes
sign_in_onenum: mainnetNile only: people sign in on the mainnet API and arrive here by pass.
email_captchaobjectPresent 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.providerenum: turnstileyes
email_captcha.site_keystringyes
email_captcha.actionstringyes

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

NameInTypeRequiredDescription
return_toquerystringA 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_touchquerystringHow the visitor first found the site (JSON, at most 2000 characters); stored once on a new account.

Responses

StatusMeaning
302To Telegram.
404No 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

NameInTypeRequiredDescription
codequerystring
statequerystring
errorquerystring

Responses

StatusMeaning
302Back to the site.
404No 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

NameInTypeRequiredDescription
return_toquerystringA 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_touchquerystringHow the visitor first found the site (JSON, at most 2000 characters); stored once on a new account.

Responses

StatusMeaning
302To Google.
404No 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

NameInTypeRequiredDescription
codequerystring
statequerystring
errorquerystring

Responses

StatusMeaning
302Back to the site.
404No 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

StatusMeaning
200OK
401Missing, malformed or rejected credentials.

Response fields (StepUpState)

FieldTypeRequiredDescription
channelenum: off, email, telegram, walletyes
availablearray of enum: email, telegram, walletyesChannels this person can switch to now.
emailobject (StepUpEmail) | nullyesWhere 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_idstringyesThe sign-in method the address comes from.
email.addressstringyesThe whole address: it is the signed-in person’s own.
email.sourceenum: email, googleyesemail: 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.
emailsarray of object (StepUpEmail)yesEvery address that can receive codes, in fallback order.
emails[].identity_idstringyesThe sign-in method the address comes from.
emails[].addressstringyesThe whole address: it is the signed-in person’s own.
emails[].sourceenum: email, googleyesemail: 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.
telegramobject | nullyesThe Telegram account the bot sends codes to; null without one or without the bot.
telegram.identity_idstringyes
telegram.displaystring | nullyesThe account’s @username or name, when Telegram gave one.
google_email_unknownbooleanyesAlways 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

NameInTypeRequiredDescription
X-Step-UpheaderstringA 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.

FieldTypeRequiredDescription
channelenum: off, email, telegram, walletyes
identity_idstringE-mail only: the emails[].identity_id codes go to.

Responses

StatusMeaning
200Saved.
400Malformed request — bad JSON, unknown field, wrong type.
401Missing, malformed or rejected credentials.
4031020 step_up_required (2-step on: approve, then repeat with X-Step-Up), 1021 step_up_invalid, 1014 csrf_token_invalid.
4091022 step_up_channel_unavailable, 1025 telegram_bot_not_started, 1026 telegram_relink_needed.

Response fields (StepUpState)

FieldTypeRequiredDescription
channelenum: off, email, telegram, walletyes
availablearray of enum: email, telegram, walletyesChannels this person can switch to now.
emailobject (StepUpEmail) | nullyesWhere 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_idstringyesThe sign-in method the address comes from.
email.addressstringyesThe whole address: it is the signed-in person’s own.
email.sourceenum: email, googleyesemail: 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.
emailsarray of object (StepUpEmail)yesEvery address that can receive codes, in fallback order.
emails[].identity_idstringyesThe sign-in method the address comes from.
emails[].addressstringyesThe whole address: it is the signed-in person’s own.
emails[].sourceenum: email, googleyesemail: 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.
telegramobject | nullyesThe Telegram account the bot sends codes to; null without one or without the bot.
telegram.identity_idstringyes
telegram.displaystring | nullyesThe account’s @username or name, when Telegram gave one.
google_email_unknownbooleanyesAlways 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

NameInTypeRequiredDescription
X-Step-UpheaderstringA 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.

FieldTypeRequiredDescription
request_idstringyesdetails.step_up_id of the Nile 1020.
payload_hashstringyesdetails.payload_hash of the Nile 1020.
action_labelstringWhat the Nile action is; shown to the person only.

Responses

StatusMeaning
200The step-up pass.
400Malformed request — bad JSON, unknown field, wrong type.
401Missing, malformed or rejected credentials.
4031020 step_up_required (approve on mainnet, then repeat with X-Step-Up), 1021 step_up_invalid, 1014 csrf_token_invalid.
404Not offered on this stack.

Response fields

FieldTypeRequiredDescription
passstringyes
expires_atstring (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

NameInTypeRequiredDescription
idpathstringyes

Request body

JSON (StepUpApproveRequest), required.

FieldTypeRequiredDescription
codestringE-mail channel.
signaturestringWallet channel: TIP-191 signature over details.message.
passstringNile, 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

StatusMeaning
200Approved.
4001016 login_code_invalid, 1017 login_code_expired, or 2001 validation_failed.
401Missing, malformed or rejected credentials.
4031021 step_up_invalid: unknown, not pending, expired, or a signature from no linked wallet.

Response fields (StepUpApproved)

FieldTypeRequiredDescription
idstringyes
statusenum: approvedyes
expires_atstring (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

StatusMeaning
200OK
401Missing, malformed or rejected credentials.
500Something broke on our side.

Response fields

FieldTypeRequiredDescription
dataarray of object (SignIn)yes
data[].signed_in_atstring (date-time)yes
data[].ipstring | nullyes
data[].addressstring | nullyesThe wallet that signed in; null for a sign-in without a wallet (e-mail code).
data[].methodstring | nullHow 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):

json
{
  "data": [
    {
      "signed_in_at": "2026-09-25T09:41:12.000Z",
      "ip": "203.0.113.10",
      "address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE"
    }
  ]
}

    ↑ ↓ to move · Enter to open · Esc to close