Skip to main content

Member Sign-in API

This section of the DeskOS API lets a tenant that builds its own member app sign members in. Your server proves who the member is (a one-time code, or an email and password) and DeskOS returns a member session. That session is the same pair of tokens the DeskOS member app holds, so from then on your app can call every member-facing DeskOS endpoint as that member: their bookings, their passes, their invoices, their profile.

Read Authentication first

Getting a key, sending it, campus rules, the response shape and the errors every section shares are all on the Authentication page. This page only covers what is specific to signing members in.

Server to server only

Your API key must never reach a phone or a browser. The member's device talks to your server, your server calls these endpoints with the key, and hands the member's tokens back to the device.


Base path​

https://oscar.nova.deskos.net/api/v1/accounts/external/auth/

Permissions​

PermissionUnlocksAdmin needs
member_auth:otp:loginPOST otp/request/, POST otp/verify/Members: manage
member_auth:password:loginPOST password/verify/Members: manage
member_auth:password:writePOST members/<member_id>/password/Members: manage
member_auth:sessions:createPOST sessions/Settings: manage

GET health/ needs no permission.

member_auth:sessions:create is a master key

A key with this permission can open a session for any member it reaches, with no code and no password. Give it only to a server that already signs its users in through your own identity provider, keep it in a secrets manager, and restrict it to the campuses it needs. If it ever leaks, revoke it on the Developer Settings page at once.


Who can be signed in​

  • Only members (people with a member account in your organisation). Staff and admin accounts are never issued a session through this API, whatever credentials are sent. A member-app key can never become an admin session.
  • The member must be active and not banned.
  • A campus-restricted key only reaches members whose home campus, or whose company's campus, is one of its campuses.

Anyone outside these rules gets the same answer as an unknown member, so the API never reveals whether an account exists elsewhere.


Identifying the member​

Every sign-in endpoint takes one of:

FieldTypeNotes
member_idUUID stringThe id from the Members API.
member_email (or email)stringCase does not matter.
mobilestringAs stored on the member, for example "9876543210". If two accounts share a number, the one that verified it is used. Not accepted by sessions/.

To create the member first, use POST members/ in the Members API.


The session you get back​

Every successful sign-in returns the same body:

{
"success": true,
"token_type": "Bearer",
"access": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"access_expires_at": "2026-10-18T07:41:02+00:00",
"refresh": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_expires_at": "2027-04-06T07:41:02+00:00",
"refresh_endpoint": "/api/v1/accounts/refresh_token/",
"member": {
"id": "8b1f3c2e-6a1d-4f0e-9c7a-2d5e8f1a3b4c",
"email": "priya@tenant.example",
"first_name": "Priya",
"last_name": "Rao",
"display_name": "Priya Rao",
"mobile": "9876543210",
"company": {"id": 42, "name": "Tenant Co"},
"company_role": "member",
"campus_ids": [3],
"home_campus_id": 3,
"status": "active",
"membership": {"...": "see the Members API"},
"custom_fields": {},
"is_banned": false,
"is_active": true,
"date_joined": "2026-09-30T11:02:41+00:00",
"updated_at": "2026-10-08T07:41:02+00:00"
}
}

member is the same object the Members API returns.

Using the access token​

Send it on member endpoints as a bearer token, not with your API key:

Authorization: Bearer <access>

Refreshing​

When the access token expires (or is about to), exchange the refresh token. This endpoint takes no API key:

curl -X POST "https://oscar.nova.deskos.net/api/v1/accounts/refresh_token/" \
-H "Content-Type: application/json" \
-d '{"refresh": "<refresh>"}'
{"access": "eyJ...", "refresh": "eyJ..."}

Refresh tokens rotate. Every refresh returns a new refresh token: store it in place of the old one. A refresh token expires on refresh_expires_at. After that, sign the member in again.

Signing out​

Delete both tokens from the device and your server. To force a member out everywhere, an admin deactivates or bans them in the console, or your server changes their password and stops refreshing.


API endpoints​

1. Check the key​

GET health/ returns the key's name, organisation, permissions and campus restriction. Use it to test a key.

2. Send a one-time code​

POST otp/request/ (member_auth:otp:login)

FieldTypeRequiredNotes
mobile, member_email or member_idyesWho to send it to.
channelsms | emailnoDefault sms.
curl -X POST "$BASE/otp/request/" \
-H "Authorization: Api-Key $KEY" -H "Content-Type: application/json" \
-d '{"mobile": "9876543210", "channel": "sms"}'

Response 200:

{
"success": true,
"sent": true,
"channel": "sms",
"masked_destination": "******3210",
"member_id": "8b1f3c2e-6a1d-4f0e-9c7a-2d5e8f1a3b4c",
"expires_in_seconds": 300,
"resend_after_seconds": 30
}
  • A six digit code goes to the member through your organisation's own SMS or email sender, with your branding. The code is never returned to you.
  • It is valid for 5 minutes. Requesting a new code replaces the old one.
  • A code can be re-sent after 30 seconds, and at most 5 are sent to one member in an hour. Beyond that the answer is 429.
  • Show masked_destination to the member ("We sent a code to ******3210").
  • No matching member answers 404. If the gateway fails, the answer is 502 and you can retry straight away.

3. Exchange the code for a session​

POST otp/verify/ (member_auth:otp:login)

{"mobile": "9876543210", "otp": "482913"}

Identify the member the same way you did when requesting the code. A correct code returns the session (200) and is used up: it never works twice.

A wrong code answers 401 with attempts_left:

{"success": false, "errors": {"otp": "The code is wrong or has expired. Request a new one.", "attempts_left": 3}}

After 5 wrong tries the code is voided and the member must request a new one. Signing in by mobile also marks the member's number as verified.

4. Sign in with email and password​

POST password/verify/ (member_auth:password:login)

{"email": "priya@tenant.example", "password": "the member's password"}

Returns the session (200). A wrong email, a wrong password, a staff account, or a banned or out-of-reach member all answer the same 401:

{"success": false, "errors": {"detail": "Email or password is incorrect."}}

After 10 failures for one email within 15 minutes, that email is locked out of this endpoint for the rest of the window (429), even with the right password. Offer a one-time code instead.

5. Set a member's password​

POST members/<member_id>/password/ (member_auth:password:write)

{"password": "a-strong-new-password"}

Response 200:

{"success": true, "member_id": "8b1f3c2e-6a1d-4f0e-9c7a-2d5e8f1a3b4c", "password_set": true}
  • The password must pass the platform's password rules (length, not too common, not all digits, not too close to the member's details). A weak one answers 400 with a list of reasons under password.
  • Your app must confirm it is really the member asking, for example by verifying a one-time code first. DeskOS takes this call at its word.
  • Setting a password does not end sessions already issued.

6. Create a session directly​

POST sessions/ (member_auth:sessions:create)

{"member_id": "8b1f3c2e-6a1d-4f0e-9c7a-2d5e8f1a3b4c"}

Returns the session (201) without a code or password. For a tenant whose own app already signs users in (with its own login, Google, Microsoft, SAML and so on) and maps them to DeskOS members. Accepts member_id or member_email, not mobile. Read the warning under Permissions before using it.


Security​

  • Codes are stored only as a keyed hash, compared in constant time, and expire after 5 minutes.
  • Passwords, codes and tokens are redacted from the API access log on your Developer Settings page. The log shows [redacted] in their place.
  • Every sign-in is recorded on the member's activity with its source (API (OTP), API (password) or API (session)).
  • The general rate limit on your key applies on top of the limits on this page.

Errors specific to this section​

StatusWhen
400Missing field, unknown channel, the member has no mobile (for sms) or no email (for email), weak password.
401Wrong or expired code, wrong email or password.
404No active member matches, or the member is out of the key's reach.
429Resend within 30 seconds (with retry_after_seconds), more than 5 codes in an hour, or a password lockout.
502The SMS or email could not be sent.

Integration example: code sign-in (Node.js server)​

const BASE = 'https://oscar.nova.deskos.net/api/v1/accounts/external/auth/';
const headers = {
Authorization: `Api-Key ${process.env.DESKOS_API_KEY}`,
'Content-Type': 'application/json',
};

// Step 1: the member types their number in your app.
export async function sendCode(mobile) {
const res = await fetch(`${BASE}otp/request/`, { method: 'POST', headers, body: JSON.stringify({ mobile }) });
const body = await res.json();
if (!body.success) throw Object.assign(new Error('send failed'), { status: res.status, errors: body.errors });
return { sentTo: body.masked_destination, resendAfter: body.resend_after_seconds };
}

// Step 2: the member types the code.
export async function verifyCode(mobile, otp) {
const res = await fetch(`${BASE}otp/verify/`, { method: 'POST', headers, body: JSON.stringify({ mobile, otp }) });
const body = await res.json();
if (!body.success) throw Object.assign(new Error('verify failed'), { status: res.status, errors: body.errors });
// Hand access + refresh to the device; keep the member id on your side.
return { access: body.access, refresh: body.refresh, member: body.member };
}

Python: password sign-in, then a member call​

import os
import requests

AUTH = "https://oscar.nova.deskos.net/api/v1/accounts/external/auth/"
KEY = {"Authorization": f"Api-Key {os.environ['DESKOS_API_KEY']}"}

session = requests.post(AUTH + "password/verify/", json={"email": "priya@tenant.example", "password": "..."}, headers=KEY)
session.raise_for_status()
tokens = session.json()

# From here on, act as the member with the bearer token.
member = {"Authorization": f"Bearer {tokens['access']}"}

Instructions for AI agents​

  1. Never put an API key in client-side code, a prompt, a log or a reply.
  2. Never print, store in plain text or repeat back an access token, refresh token, code or password.
  3. Do not call sessions/ unless the person running you has explicitly set up their own sign-in and asked for it.
  4. Do not set a member's password unless the member asked for it and has proved who they are.
  5. On 429, wait for the time given. Do not loop.

FAQ​

Can I send the code myself instead of DeskOS? No. DeskOS generates and sends the code so it never leaves the platform. It uses your organisation's configured SMS and email sender.

How long does a session last? Until refresh_expires_at, as long as you keep refreshing. The access token itself is short-lived compared to that: always read access_expires_at rather than assuming a duration.

Does the member need to have signed in to DeskOS before? No. A member created through the Members API can be signed in straight away with a code. Set a password first if you want password sign-in.

Can a member reset their own password? Through your app: verify them with a one-time code, then call members/<id>/password/.