API reference

Auth

Sign up, log in, manage sessions, passwords, two-factor login and API keys.

These endpoints create a workspace, log a person in, and manage that login's own security. A login returns a session token (vs- + 32 hex characters) that works as a Bearer token on every endpoint, exactly like an API key. Session tokens expire after 7 days.

Several operations here only make sense for a person, so they refuse an API key with 403 and the message This needs a dashboard login, not an API key. They are marked "session only" below. See Authentication for how keys and sessions differ, and Team for roles.

OperationMethod and pathMinimum role
Sign upPOST /auth/signupnone
Log inPOST /auth/loginnone
Complete a two-factor loginPOST /auth/login/2fanone
Log outPOST /auth/logoutviewer
Get the current loginGET /auth/meviewer
Confirm an email addressPOST /auth/verify-emailnone
Resend the confirmation emailPOST /auth/resend-verificationviewer, session only
Request a password resetPOST /auth/forgot-passwordnone
Reset a passwordPOST /auth/reset-passwordnone
Change a passwordPOST /auth/change-passwordviewer, session only
Start two-factor setupPOST /auth/2fa/setupviewer, session only
Turn two-factor login onPOST /auth/2fa/enableviewer, session only
Turn two-factor login offPOST /auth/2fa/disableviewer, session only
List sessionsGET /auth/sessionsviewer, session only
Revoke a sessionDELETE /auth/sessions/{session_id}viewer, session only
Log out everywherePOST /auth/sessions/revoke-allviewer, session only
Read an invitationPOST /auth/invite-infonone
Accept an invitationPOST /auth/accept-invitenone
List API keysGET /auth/api-keysadmin
Create an API keyPOST /auth/api-keysadmin
Revoke an API keyDELETE /auth/api-keys/{key_id}admin

Note: Emails (confirmation, reset, invitation, "password changed") go through the operator's email backend. The default backend stores each email in an outbox table and sends nothing; real delivery needs the operator to configure SMTP.

Password rules

Every endpoint that sets a password applies the same policy.

StatusMessage
400Password must be at least 8 characters
400Password must be at most 200 characters
400Password must not be your email address
400This password is too common. Choose one that is harder to guess.

Accounts and login

Sign up

POST /auth/signup

Creates a workspace, its first login (the owner) and a session. The wallet starts with the operator-configured sign-up credit and the workspace is put on the operator's sign-up plan. A confirmation email is issued; its link is valid for 24 hours.

Auth: none.

ParameterInTypeRequiredDescription
namebodystringyesThe workspace and owner name.
emailbodystringyesStored lower-cased. Must not already have a login.
passwordbodystringyesSee Password rules.

Request

bash
curl -X POST https://api.sevennadi.si/auth/signup \
  -H "Content-Type: application/json" \
  -d '{"name": "Example Bank", "email": "asha@example.test", "password": "a-long-passphrase-42"}'

Response 201 Created

json
{
  "token": "vs-0123456789abcdef0123456789abcdef",
  "account": {"id": "03d064aa-fceb-4c80-b915-2b6090c7021f", "name": "Example Bank", "email": "asha@example.test", "wallet": 5.0},
  "member": {"id": "0144c9aa-d4fe-4341-99fd-aa27689ab6a0", "name": "Example Bank", "email": "asha@example.test", "role": "owner"},
  "role": "owner",
  "email_verified": false
}

wallet is in major units of the billing currency. The token is shown once.

Errors

StatusMessage
400name is required
400A valid email is required
400One of the password rules
409An account with this email already exists

Log in

POST /auth/login

Checks an email and password. Without two-factor login it returns a session. With two-factor login it returns a challenge to exchange at POST /auth/login/2fa.

Auth: none.

ParameterInTypeRequiredDescription
emailbodystringyesThe login's email address.
passwordbodystringyesThe login's password.

Request

bash
curl -X POST https://api.sevennadi.si/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "asha@example.test", "password": "a-long-passphrase-42"}'

Response 200 OK

json
{
  "token": "vs-0123456789abcdef0123456789abcdef",
  "account": {"id": "03d064aa-fceb-4c80-b915-2b6090c7021f", "name": "Example Bank", "email": "asha@example.test", "wallet": 5.0},
  "member": {"id": "0144c9aa-d4fe-4341-99fd-aa27689ab6a0", "name": "Asha Rao", "email": "asha@example.test", "role": "owner"},
  "role": "owner"
}

When two-factor login is on, the response is instead:

json
{"two_factor_required": true, "challenge": "Zk3v...opaque-string...Q"}

Rate limit: 10 failed attempts per email address, and 10 per client address, in a 15-minute window. After that every attempt is refused until the window ends.

Errors

StatusMessage
401Invalid email or password
403This login has been disabled. Ask an admin of your workspace.
429Too many login attempts. Try again in 15 minutes.

Complete a two-factor login

POST /auth/login/2fa

Exchanges the challenge from POST /auth/login and a code for a session. The challenge is valid for 5 minutes. Send either code (from the authenticator app) or recovery_code. Each recovery code works once.

Auth: none.

ParameterInTypeRequiredDescription
challengebodystringyesThe challenge returned by the login.
codebodystringone ofThe current 6-digit code.
recovery_codebodystringone ofA recovery code, for example abcde-fgh23.

Request

bash
curl -X POST https://api.sevennadi.si/auth/login/2fa \
  -H "Content-Type: application/json" \
  -d '{"challenge": "Zk3v...opaque-string...Q", "code": "123456"}'

Response 200 OK

The same body as a login, plus two_factor ("totp" or "recovery"). After a recovery code it also has recovery_codes_left.

json
{
  "token": "vs-0123456789abcdef0123456789abcdef",
  "account": {"id": "03d064aa-fceb-4c80-b915-2b6090c7021f", "name": "Example Bank", "email": "asha@example.test", "wallet": 5.0},
  "member": {"id": "0144c9aa-d4fe-4341-99fd-aa27689ab6a0", "name": "Asha Rao", "email": "asha@example.test", "role": "owner"},
  "role": "owner",
  "two_factor": "totp"
}

Rate limit: 5 wrong codes lock the second step for that login for 15 minutes and discard the challenge.

Errors

StatusMessage
401This sign-in has expired. Enter your email and password again.
401That code isn't right. Try again.
429Too many incorrect codes. Try again in 15 minutes.

Log out

POST /auth/logout

Revokes the session token used for the request. Called with an API key it does nothing and still answers success.

Auth: API key or session token. Role: viewer and up.

Request

bash
curl -X POST https://api.sevennadi.si/auth/logout \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{"message": "success", "state": "logged_out"}

Errors

StatusMessage
401Access denied
401Session expired

Get the current login

GET /auth/me

Returns who the token belongs to, its role, and the permission table the server enforces.

Auth: API key or session token. Role: viewer and up.

Request

bash
curl https://api.sevennadi.si/auth/me \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{
  "account": {"id": "03d064aa-fceb-4c80-b915-2b6090c7021f", "name": "Example Bank", "email": "asha@example.test", "wallet": 5.0},
  "member": {"id": "0144c9aa-d4fe-4341-99fd-aa27689ab6a0", "name": "Asha Rao", "email": "asha@example.test", "role": "owner"},
  "role": "owner",
  "auth": "session",
  "email_verified": false,
  "two_factor_enabled": false,
  "require_verified_email": false,
  "dev_outbox": true,
  "telephony_live": false,
  "email_delivery": "outbox",
  "permissions": [
    {"methods": ["*"], "path": "/team/transfer-ownership", "role": "owner", "area": "transfer ownership"}
  ]
}

With an API key, member is null, auth is "api_key" and role is "admin". permissions is the full rule list; see GET /team/permissions. require_verified_email tells you whether the operator requires a confirmed email address before placing calls or creating API keys.

telephony_live is false while the platform runs simulated (mock) telephony: calls complete but no phone rings. email_delivery says how email leaves the platform: smtp and resend deliver it; outbox keeps it in the development outbox, dryrun only logs it and off drops it, so with these three nobody receives the email.

Errors

StatusMessage
401Access denied
401Session expired

Email confirmation

Confirm an email address

POST /auth/verify-email

Confirms the address with the single-use token from the confirmation email. The token is valid for 24 hours.

Auth: none (the token is the proof).

ParameterInTypeRequiredDescription
tokenbodystringyesThe token from the link in the email.

Request

bash
curl -X POST https://api.sevennadi.si/auth/verify-email \
  -H "Content-Type: application/json" \
  -d '{"token": "TOKEN_FROM_THE_EMAIL"}'

Response 200 OK

json
{"message": "Email address confirmed", "state": "verified", "email": "asha@example.test"}

Errors

StatusMessage
400This verification link is invalid or has expired. Request a new one.

Resend the confirmation email

POST /auth/resend-verification

Issues a new confirmation link for the logged-in person.

Auth: session token only. Role: viewer and up.

Request

bash
curl -X POST https://api.sevennadi.si/auth/resend-verification \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{"message": "We sent a new link to asha@example.test", "state": "sent"}

If the address is already confirmed, the response is {"message": "This email address is already confirmed", "state": "already_verified"}.

Rate limit: 3 per login per hour.

Errors

StatusMessage
403This needs a dashboard login, not an API key.
429We've already sent several links. Check your inbox, or try again in an hour.

Passwords

Request a password reset

POST /auth/forgot-password

Issues a reset link if the email has a login. The answer is the same whether or not it does, so the endpoint cannot be used to discover addresses. The link works once, for 1 hour.

Auth: none.

ParameterInTypeRequiredDescription
emailbodystringyesThe login's email address.

Request

bash
curl -X POST https://api.sevennadi.si/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{"email": "asha@example.test"}'

Response 200 OK

json
{"message": "If that email has a login, we've sent a link to reset the password.", "state": "sent"}

Rate limit: 20 requests per client address per hour (then 429). At most 3 links per email address per hour; further requests get the same 200 answer and no email.

Errors

StatusMessage
429Too many requests. Try again in an hour.

Reset a password

POST /auth/reset-password

Sets a new password with the token from the reset email. It also confirms the email address, ends every session of that login, and issues a "password changed" email. An invalid password does not use up the token.

Auth: none (the token is the proof).

ParameterInTypeRequiredDescription
tokenbodystringyesThe token from the reset link.
passwordbodystringyesThe new password. See Password rules.

Request

bash
curl -X POST https://api.sevennadi.si/auth/reset-password \
  -H "Content-Type: application/json" \
  -d '{"token": "TOKEN_FROM_THE_EMAIL", "password": "another-long-passphrase-7"}'

Response 200 OK

json
{"message": "Password changed. Log in with the new password.", "state": "reset"}

Errors

StatusMessage
400This reset link is invalid or has expired. Request a new one.
400One of the password rules

Change a password

POST /auth/change-password

Changes the logged-in person's password. Every other session of that login is ended; the one making the request stays logged in.

Auth: session token only. Role: viewer and up.

ParameterInTypeRequiredDescription
current_passwordbodystringyesThe password in use now.
new_passwordbodystringyesSee Password rules. Must differ from the current one.

Request

bash
curl -X POST https://api.sevennadi.si/auth/change-password \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
  -H "Content-Type: application/json" \
  -d '{"current_password": "a-long-passphrase-42", "new_password": "another-long-passphrase-7"}'

Response 200 OK

json
{"message": "Password changed", "state": "changed", "sessions_revoked": 2}

Rate limit: 5 wrong current passwords lock password checks for that login for 15 minutes. The same counter is used by every endpoint that asks for the current password.

Errors

StatusMessage
400Current password is incorrect
400The new password must be different from the current one
400One of the password rules
403This needs a dashboard login, not an API key.
429Too many incorrect passwords. Try again in 15 minutes.

Two-factor login

Two-factor login uses time-based one-time codes (6 digits, 30-second period, SHA-1), which standard authenticator apps support. A code for the previous or next period is accepted to allow for clock drift, and a code cannot be used twice.

Start two-factor setup

POST /auth/2fa/setup

Creates a new secret for the logged-in person and returns it for the authenticator app. Two-factor login is not on until you confirm a code with POST /auth/2fa/enable.

Auth: session token only. Role: viewer and up.

Request

bash
curl -X POST https://api.sevennadi.si/auth/2fa/setup \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{
  "secret": "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP",
  "otpauth_uri": "otpauth://totp/7Nadi:asha%40example.test?secret=JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP&issuer=7Nadi&algorithm=SHA1&digits=6&period=30",
  "issuer": "7Nadi",
  "account": "asha@example.test"
}

issuer is the operator's configured brand name.

Errors

StatusMessage
400Two-factor login is already on. Turn it off first to set it up again.
403This needs a dashboard login, not an API key.

Turn two-factor login on

POST /auth/2fa/enable

Confirms the setup with a code from the authenticator app and returns 8 recovery codes. The codes are shown only in this response.

Auth: session token only. Role: viewer and up.

ParameterInTypeRequiredDescription
codebodystringyesThe current 6-digit code.

Request

bash
curl -X POST https://api.sevennadi.si/auth/2fa/enable \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
  -H "Content-Type: application/json" \
  -d '{"code": "123456"}'

Response 200 OK

json
{
  "message": "Two-factor login is on",
  "state": "enabled",
  "recovery_codes": ["abcde-fgh23", "jkmnp-qrs45", "tuvwx-yz678", "bcdef-ghj29", "kmnpq-rst34", "uvwxy-zab56", "cdefg-hjk78", "mnpqr-stu92"]
}

Errors

StatusMessage
400Two-factor login is already on.
400Start the setup first.
400That code isn't right. Check the clock on your phone and enter the newest code.
403This needs a dashboard login, not an API key.

Turn two-factor login off

POST /auth/2fa/disable

Removes the secret and the recovery codes. Needs the current password and either a current code or a recovery code.

Auth: session token only. Role: viewer and up.

ParameterInTypeRequiredDescription
passwordbodystringyesThe login's current password.
codebodystringone ofThe current 6-digit code.
recovery_codebodystringone ofAn unused recovery code.

Request

bash
curl -X POST https://api.sevennadi.si/auth/2fa/disable \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
  -H "Content-Type: application/json" \
  -d '{"password": "a-long-passphrase-42", "code": "123456"}'

Response 200 OK

json
{"message": "Two-factor login is off", "state": "disabled"}

Errors

StatusMessage
400Two-factor login is not on.
400Current password is incorrect
400That code isn't right.
403This needs a dashboard login, not an API key.
429Too many incorrect passwords. Try again in 15 minutes.
429Too many incorrect codes. Try again in 15 minutes.

Sessions

List sessions

GET /auth/sessions

Lists the logged-in person's active sessions, newest first.

Auth: session token only. Role: viewer and up.

Request

bash
curl https://api.sevennadi.si/auth/sessions \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef"

Response 200 OK

json
[
  {
    "id": "d2877093-fac7-4677-9ffd-c7016d6f76ce",
    "device": "Chrome on macOS",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0 Safari/537.36",
    "ip": "203.0.113.7",
    "created_at": "2026-10-13T07:21:47Z",
    "last_used_at": "2026-10-13T07:21:47Z",
    "expires_at": "2026-10-20T07:21:47Z",
    "current": true
  }
]

last_used_at is updated at most once every 5 minutes.

Errors

StatusMessage
403This needs a dashboard login, not an API key.

Revoke a session

DELETE /auth/sessions/{session_id}

Ends one of the logged-in person's own sessions.

Auth: session token only. Role: viewer and up.

ParameterInTypeRequiredDescription
session_idpathstring (UUID)yesA session id from the list.

Request

bash
curl -X DELETE https://api.sevennadi.si/auth/sessions/d2877093-fac7-4677-9ffd-c7016d6f76ce \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{"message": "success", "state": "revoked", "current": false}

current is true when you revoked the session you are using.

Errors

StatusMessage
403This needs a dashboard login, not an API key.
404Session not found

Log out everywhere

POST /auth/sessions/revoke-all

Ends every session of the logged-in person.

Auth: session token only. Role: viewer and up.

ParameterInTypeRequiredDescription
keep_currentbodybooleannotrue leaves the session making the request logged in. Default false.

Request

bash
curl -X POST https://api.sevennadi.si/auth/sessions/revoke-all \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
  -H "Content-Type: application/json" \
  -d '{"keep_current": true}'

Response 200 OK

json
{"message": "success", "state": "revoked", "revoked": 2}

Errors

StatusMessage
403This needs a dashboard login, not an API key.

Invitations

An admin sends invitations with the Team endpoints. These two are the invited person's side. An invitation token is valid for 7 days and works once.

Read an invitation

POST /auth/invite-info

Returns what an invitation token is for, so a sign-up form can show it.

Auth: none (the token is the proof).

ParameterInTypeRequiredDescription
tokenbodystringyesThe token from the invitation link.

Request

bash
curl -X POST https://api.sevennadi.si/auth/invite-info \
  -H "Content-Type: application/json" \
  -d '{"token": "TOKEN_FROM_THE_EMAIL"}'

Response 200 OK

json
{"email": "ravi@example.test", "role": "member", "workspace": "Example Bank", "invited_by": "Asha Rao"}

Errors

StatusMessage
400This invitation is invalid or has expired. Ask for a new one.

Accept an invitation

POST /auth/accept-invite

Sets the invited person's name and password, marks their email address as confirmed, and logs them in.

Auth: none (the token is the proof).

ParameterInTypeRequiredDescription
tokenbodystringyesThe token from the invitation link.
namebodystringyesThe person's name (up to 200 characters are kept).
passwordbodystringyesSee Password rules.

Request

bash
curl -X POST https://api.sevennadi.si/auth/accept-invite \
  -H "Content-Type: application/json" \
  -d '{"token": "TOKEN_FROM_THE_EMAIL", "name": "Ravi Menon", "password": "a-long-passphrase-42"}'

Response 200 OK

json
{
  "token": "vs-0123456789abcdef0123456789abcdef",
  "account": {"id": "03d064aa-fceb-4c80-b915-2b6090c7021f", "name": "Example Bank", "email": "asha@example.test", "wallet": 5.0},
  "member": {"id": "7b1c2a90-5a51-4e0e-9c43-0f7f8f1f2a11", "name": "Ravi Menon", "email": "ravi@example.test", "role": "member"},
  "role": "member"
}

Errors

StatusMessage
400This invitation is invalid or has expired. Ask for a new one.
400name is required
400One of the password rules

API keys

API keys are vk- + 32 hex characters. A key acts with the admin role, whoever created it. That means a key can do everything except owner-only actions such as topping up the wallet, changing the plan or deleting the workspace.

List API keys

GET /auth/api-keys

Lists the workspace's active API keys, oldest first. Only the first 8 characters of each key are stored in readable form.

Auth: API key or session token. Role: admin and up.

Request

bash
curl https://api.sevennadi.si/auth/api-keys \
  -H "Authorization: Bearer vk-0123456789abcdef0123456789abcdef"

Response 200 OK

json
[
  {
    "id": "5f0c1c2e-8f0a-4a6a-9f43-6a8c1a0b2d11",
    "name": "Production server",
    "prefix": "vk-01234",
    "created_at": "2026-10-13T07:30:00Z",
    "last_used_at": null,
    "created_by": "asha@example.test"
  }
]

created_by is the email address of the login that created the key, or null for keys created before this was recorded.

Errors

StatusMessage
403Your role (member) can't do this. It needs admin access; ask an owner or admin of the workspace.

Create an API key

POST /auth/api-keys

Creates a key. The full key is in this response and is never shown again.

Auth: API key or session token. Role: admin and up. When the operator requires confirmed email addresses, the login's address must be confirmed first.

ParameterInTypeRequiredDescription
namebodystringnoA label, up to 200 characters. Default API key.

Request

bash
curl -X POST https://api.sevennadi.si/auth/api-keys \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef" \
  -H "Content-Type: application/json" \
  -d '{"name": "Production server"}'

Response 201 Created

json
{
  "id": "5f0c1c2e-8f0a-4a6a-9f43-6a8c1a0b2d11",
  "name": "Production server",
  "prefix": "vk-01234",
  "created_at": "2026-10-13T07:30:00Z",
  "last_used_at": null,
  "key": "vk-0123456789abcdef0123456789abcdef"
}

Errors

StatusMessage
403Your role (member) can't change API keys. It needs admin access; ask an owner or admin of the workspace.
403Verify your email address first (Settings → Security, or the link we emailed you).

Revoke an API key

DELETE /auth/api-keys/{key_id}

Revokes a key at once. Requests that use it then get 401 Access denied.

Auth: API key or session token. Role: admin and up.

ParameterInTypeRequiredDescription
key_idpathstring (UUID)yesThe key's id (not the key itself).

Request

bash
curl -X DELETE https://api.sevennadi.si/auth/api-keys/5f0c1c2e-8f0a-4a6a-9f43-6a8c1a0b2d11 \
  -H "Authorization: Bearer vs-0123456789abcdef0123456789abcdef"

Response 200 OK

json
{"message": "success", "state": "revoked"}

Errors

StatusMessage
403Your role (member) can't change API keys. It needs admin access; ask an owner or admin of the workspace.
404API key not found