Skip to main content

Members API

This section of the DeskOS API covers members, companies and membership status. From an outside system such as a WiFi controller, an access-control platform, an HR or CRM tool, n8n or Zapier, or an AI agent you can list every member and company in a workspace, read whether a member's membership is live right now, keep your own copy in sync as things change, create and update members and companies, and write your own values back onto a member through custom fields.

The typical use is a vendor integration. A WiFi provider lists the members at a campus, checks who currently holds a live subscription, generates a PIN for each of them, and writes that PIN back onto the member as a custom field so the operator's staff can see it on the member's profile in DeskOS.

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 members.

Building with an AI agent or coding assistant?

Read the general agent instructions on the Authentication page, then the member-specific rules below.


Base path​

All paths on this page are relative to:

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

Your Developer Settings page shows the exact base URL for your workspace.


Permissions​

Member permissions all start with members:. An admin ticks them per key on the Developer Settings page. A key only gets member permissions if the admin creating it holds Members: manage themselves.

PermissionWhat it allowsEndpoints
members:members:readRead membersGET members/, GET members/<id>/
members:members:writeCreate and update members, deactivate and reactivate themPOST members/, PATCH members/<id>/, POST members/<id>/deactivate/, POST members/<id>/reactivate/
members:companies:readRead companiesGET companies/, GET companies/<id>/
members:companies:writeCreate and update companiesPOST companies/, PATCH companies/<id>/
members:memberships:readRead subscriptions and one member's live statusGET memberships/, GET members/<id>/membership/
members:custom_fields:readRead the custom field definitionsGET custom-fields/
members:custom_fields:writeWrite custom field valuesPATCH members/<id>/custom-fields/, PATCH companies/<id>/custom-fields/
members:campuses:readRead the campus listGET campuses/

A typical vendor integration ("provision our service for whoever is a live member") needs members:members:read, members:memberships:read, members:custom_fields:read and members:custom_fields:write. It does not need either write permission on members or companies.

A directory sync ("push our HR joiners into DeskOS") needs members:members:read, members:members:write and members:companies:read.

GET health/ needs no permission and works with any key. Use it to confirm the key carries the members: permissions you expect before doing anything else.

Members are never deleted through this API

There is no delete endpoint for a member or a company, and there never will be. POST members/<id>/deactivate/ is the strongest thing an outside system can do: the member can no longer sign in, and nothing is erased. A bug in an integration cannot destroy an operator's records.


Campuses​

Members, companies and custom field definitions all belong to a campus. The general campus rules are on the Authentication page. For this section:

  • List endpoints accept ?campus_id= to narrow results. Without it they return everything the key can reach.
  • A member is visible to a campus-restricted key if the member belongs to one of the key's campuses or their company is homed at one. A member who switched campus is never lost to the vendor that provisions them.
  • Create resolves the campus in this order: campus_id in the body, else the company's home campus (for a member), else the key's default campus, else the organisation's only campus. If none of those applies you get a 400 asking for campus_id.
  • Custom field definitions are per campus. A field named wifi_pin at Acme North is a different definition from wifi_pin at Acme South. Fetch GET custom-fields/ for the campus you are about to write to.

Membership status​

Every member and company payload carries a status and a membership object. Status is computed on every read, never stored. It answers one question: does this person hold a live subscription at this campus right now?

StatusMeaning
activeThe member's company holds a live subscription at the campus
on_noticeThe subscription is live but running out its notice period, so it ends soon
pendingThe account exists but is still waiting for the operator to approve it
inactiveNo live subscription, or the account has been deactivated
bannedThe operator has banned this member

on_notice still means the member may use the space today. Treat active and on_notice together as "entitled", and pending, inactive and banned as "not entitled".

The membership object says why, and which subscription the answer came from:

{
"status": "active",
"reason": "entitled",
"campus_id": 7,
"start": "2026-09-01T00:00:00+05:30",
"end": "2026-12-31T23:59:59+05:30",
"subscription_id": "8f0b2a0e-3c1d-4e2b-9a77-2c1f5b7d4a10",
"kind": "time_based"
}

Reason codes, in the order they are checked:

ReasonMeaning
bannedThe member is banned
deactivatedThe member account has been deactivated
pending_approvalThe member is waiting for approval
no_companyThe member is not attached to a company
no_campusThere is no campus to evaluate the member at
no_active_subscriptionThe company has no live subscription at this campus
on_noticeLive, but serving its notice period
entitledThe company holds a live subscription at this campus

kind is time_based (a desk or cabin subscription) or visit_based (a day-pass or visit pack). When a company holds several live subscriptions, the one whose window runs longest is the one reported, so end covers the member's whole entitlement.

Status is campus-specific

A subscription sold at one campus does not entitle a member at another. GET members/<id>/membership/?campus_id= evaluates at whichever campus you ask about; without campus_id it uses the member's current campus.


API endpoints​

MethodPathPermissionPurpose
GEThealth/noneWhat this key is and can do
GETcampuses/members:campuses:readLocations this key can reach
GETcustom-fields/members:custom_fields:readField definitions for members and companies
GETmembers/members:members:readList and filter members
POSTmembers/members:members:writeCreate a member on a company
GETmembers/<id>/members:members:readOne member
PATCHmembers/<id>/members:members:writeUpdate a member
POSTmembers/<id>/deactivate/members:members:writeStop a member signing in
POSTmembers/<id>/reactivate/members:members:writeUndo a deactivation
GETmembers/<id>/membership/members:memberships:readComputed status at a campus
PATCHmembers/<id>/custom-fields/members:custom_fields:writeWrite custom field values
GETcompanies/members:companies:readList and filter companies
POSTcompanies/members:companies:writeCreate a company
GETcompanies/<id>/members:companies:readOne company
PATCHcompanies/<id>/members:companies:writeUpdate a company
PATCHcompanies/<id>/custom-fields/members:custom_fields:writeWrite custom field values
GETmemberships/members:memberships:readList subscriptions

Member ids are UUIDs. Company ids are integers. Both are returned on every payload.


1. List campuses​

GET /api/v1/accounts/external/campuses/
{
"success": true,
"count": 2,
"campuses": [
{
"id": 7,
"name": "Acme North",
"city": "Bengaluru",
"address": "12 MG Road",
"state": "Karnataka",
"country": "IN",
"timezone": "Asia/Kolkata",
"currency": "INR"
}
]
}

2. List custom field definitions​

GET /api/v1/accounts/external/custom-fields/
GET /api/v1/accounts/external/custom-fields/?campus_id=7

Returns the active definitions for both members and companies. The name is the slug you use when writing a value.

{
"success": true,
"members": [
{
"id": 12,
"campus_id": 7,
"label": "WiFi PIN",
"name": "wifi_pin",
"field_type": "text",
"options": [],
"has_expiry": false,
"is_required": false,
"ask_on_create": false,
"show_to_user": true
}
],
"companies": []
}

field_type is one of text, boolean, date, number, select, url. A select field lists its permitted values in options. A boolean is written as true or false. A date is YYYY-MM-DD. A url must start with http:// or https://.

If the field you want does not exist yet, an admin creates it in the DeskOS console: Memberships, then the gear icon on the Members or Companies tab. Turn on Show to member if the value should appear on the member's own dashboard.


3. List members​

GET /api/v1/accounts/external/members/
GET /api/v1/accounts/external/members/?campus_id=7&status=active,on_notice
GET /api/v1/accounts/external/members/?updated_since=2026-09-13T06:00:00Z
FilterNotes
campus_idMembers at this campus, or whose company is homed there
company_idMembers of one company
statusComma-separated: active, on_notice, pending, inactive, banned
emailExact match, case-insensitive
updated_sinceISO 8601 datetime. Everything changed at or after this moment
page, page_sizepage_size defaults to 50, maximum 200
{
"success": true,
"count": 143,
"page": 1,
"page_size": 50,
"has_next": true,
"members": [
{
"id": "3f2b6c14-77a1-4d6e-8a19-9c0e7b5a1234",
"email": "ada@widgets.example",
"first_name": "Ada",
"last_name": "Lovelace",
"display_name": "Ada Lovelace",
"mobile": "9876500011",
"company": {"id": 41, "name": "Widgets Ltd"},
"company_role": "admin",
"campus_ids": [7],
"home_campus_id": 7,
"status": "active",
"membership": {
"status": "active",
"reason": "entitled",
"campus_id": 7,
"start": "2026-09-01T00:00:00+05:30",
"end": "2026-12-31T23:59:59+05:30",
"subscription_id": "8f0b2a0e-3c1d-4e2b-9a77-2c1f5b7d4a10",
"kind": "time_based"
},
"custom_fields": {
"wifi_pin": {"value": "4821", "expires_at": null}
},
"is_banned": false,
"is_active": true,
"date_joined": "2026-04-02T11:04:22+05:30",
"updated_at": "2026-09-13T09:15:00+05:30"
}
]
}

company_role is admin (a company's own administrator, who can manage their colleagues in the member portal) or member.

Keeping a copy in sync

Store the highest updated_at you have seen, then poll ?updated_since= with that value. Editing a member, moving them between companies, deactivating them, and writing a custom field value all move updated_at, so a poller sees every change. A subscription starting or ending does not move it, because status is computed rather than stored, so re-check status on the members you care about (or poll GET memberships/?updated_since=) rather than relying on updated_at alone.


4. Create a member​

POST /api/v1/accounts/external/members/
{
"first_name": "Grace",
"last_name": "Hopper",
"email": "grace@widgets.example",
"mobile": "9876500022",
"company_id": 41,
"company_role": "member",
"custom_fields": {"wifi_pin": "7734"}
}

first_name, last_name, email and company_id are required. The member is created on the company's home campus unless you send campus_id.

Email and mobile must be unique within the organisation. The same email may exist in a different DeskOS workspace. A member created this way is approved immediately and appears in the admin console exactly as if a staff member had added them, including the entry in the company's activity log, which records the name of the key that did it.

Any custom field an admin has marked Required and Ask when creating must carry a value, or the create is refused and nothing is written:

{
"success": false,
"errors": {"custom_fields": {"wifi_pin": "This field is required."}}
}

Answers 201 with the full member object under member.


5. Update a member​

PATCH /api/v1/accounts/external/members/<id>/

Send only the keys you want to change: first_name, last_name, email, mobile, company_role, company_id, custom_fields.

{"mobile": "9876500099", "company_id": 52}

Moving a member to another company adds that company's home campus to the member and writes the move to both companies' activity logs. Send "mobile": "" to clear a mobile number.


6. Deactivate and reactivate​

POST /api/v1/accounts/external/members/<id>/deactivate/
POST /api/v1/accounts/external/members/<id>/reactivate/

No body. Both are idempotent and report whether anything actually changed:

{"success": true, "changed": true, "member": {"...": "..."}}

A deactivated member cannot sign in and reads as status: "inactive" with reason: "deactivated". Their record, history and invoices are untouched.


7. Read one member's membership​

GET /api/v1/accounts/external/members/<id>/membership/
GET /api/v1/accounts/external/members/<id>/membership/?campus_id=9

The cheapest way to answer "may this person use the space right now". Returns the same membership object as the member payload, plus a human-readable label:

{
"success": true,
"member_id": "3f2b6c14-77a1-4d6e-8a19-9c0e7b5a1234",
"status": "active",
"membership": {
"status": "active",
"reason": "entitled",
"campus_id": 7,
"start": "2026-09-01T00:00:00+05:30",
"end": "2026-12-31T23:59:59+05:30",
"subscription_id": "8f0b2a0e-3c1d-4e2b-9a77-2c1f5b7d4a10",
"kind": "time_based",
"label": "Company holds a live subscription at this campus"
}
}

8. Write custom field values onto a member​

PATCH /api/v1/accounts/external/members/<id>/custom-fields/

The short form, keyed by the field name:

{"wifi_pin": "4821"}

The long form, when you need an expiry date or want to name the campus explicitly:

{
"campus_id": 7,
"custom_fields": [
{"field_id": 12, "value": "4821", "expires_at": "2027-03-31"}
]
}

Values are validated against the definitions on the member's current campus unless campus_id says otherwise. Writing the same field twice updates the value rather than adding a second one. A field an admin marked Required cannot be cleared to an empty value. An unknown field name is a 400, so a typo never silently does nothing.

expires_at is only stored on a definition that has expiry turned on. When it is, DeskOS reminds the operator's staff before the value lapses, which is how "this member's WiFi PIN expires at the end of the quarter" gets surfaced without your system having to chase it.


9. Companies​

GET   /api/v1/accounts/external/companies/?campus_id=7&status=active&name=widgets
POST /api/v1/accounts/external/companies/
GET /api/v1/accounts/external/companies/<id>/
PATCH /api/v1/accounts/external/companies/<id>/
PATCH /api/v1/accounts/external/companies/<id>/custom-fields/
{
"id": 41,
"name": "Widgets Ltd",
"slug": "widgets-ltd",
"industry": "technology",
"website": "https://widgets.example",
"address": "Level 4, 12 MG Road",
"campus_id": 7,
"member_count": 18,
"status": "active",
"is_active": true,
"membership": {"...": "..."},
"billing": {
"legal_entity_name": "Widgets Private Limited",
"gst_number": "29AABBC1234D1Z5",
"address_line_1": "Level 4, Prestige Tower",
"address_line_2": "MG Road",
"city": "Bengaluru",
"state": "Karnataka",
"pincode": "560001",
"country": "India",
"is_default": true
},
"custom_fields": {"gst_number": {"value": "29AABBC1234D1Z5", "expires_at": null}},
"added_on": "2026-01-11T10:00:00+05:30",
"updated_on": "2026-09-10T16:40:00+05:30"
}

member_count counts active members only. A company's status is the same computation as a member's, evaluated at its home campus, so it answers "is this company a paying tenant right now". is_active is the same answer as a boolean, and it is true for on_notice as well as active, because a company serving its notice period is still paying until the period ends.

billing is the company's legal identity: the legal entity name, the tax number and the billing address that goes on its invoices. A company can hold several addresses; the one flagged as default is the one returned, and where none is flagged it is the first the operator entered. It is null for a company that has never had one filled in. This is the customer's identity today. For the identity as it stood on a particular document, read billing_to on the invoice itself in the Billing API.

Creating a company needs name, plus campus_id when the organisation has more than one campus. website, industry, address and custom_fields are optional. Company list filters are campus_id, status (active, on_notice, inactive), name (contains, case-insensitive), updated_since, page and page_size.


10. List memberships​

GET /api/v1/accounts/external/memberships/?campus_id=7&kind=time_based&status=active

The subscriptions themselves, rather than the computed per-member answer. Use it to build a renewals view, or to poll for subscriptions that changed.

FilterNotes
campus_idSubscriptions valid at this campus
company_idOne company's subscriptions
kindtime_based or visit_based
statusComma-separated raw subscription statuses: active, cancelled, expired, pending, upcoming
updated_sinceISO 8601 datetime
page, page_sizepage_size defaults to 50, maximum 200
{
"success": true,
"count": 24,
"page": 1,
"page_size": 50,
"has_next": false,
"memberships": [
{
"id": "8f0b2a0e-3c1d-4e2b-9a77-2c1f5b7d4a10",
"kind": "time_based",
"status": "active",
"on_notice": false,
"company": {"id": 41, "name": "Widgets Ltd"},
"campus_id": 7,
"start": "2026-09-01T00:00:00+05:30",
"end": "2026-12-31T23:59:59+05:30",
"total_seats": 6,
"billing_cycle": "monthly",
"updated_at": "2026-09-01T09:00:00+05:30"
},
{
"id": "b2d1f77c-1a44-4f01-9d3e-7c5b2a8e0011",
"kind": "visit_based",
"status": "active",
"on_notice": false,
"company": {"id": 55, "name": "Northwind"},
"campus_id": 7,
"start": "2026-09-05T00:00:00+05:30",
"end": "2026-10-05T23:59:59+05:30",
"total_visits": 10,
"remaining_visits": 4,
"updated_at": "2026-09-12T14:20:00+05:30"
}
]
}

status here is the subscription's own status, which is not the same vocabulary as a member's computed status. A subscription with status: "active" whose window has already passed does not entitle anyone; if you want the entitlement answer, read the member's membership instead.


A vendor integration, end to end​

This is the shape of a WiFi, access-control or printing integration.

Once, at onboarding. The operator creates a key in Developer Settings with members:members:read, members:memberships:read, members:custom_fields:read and members:custom_fields:write, restricted to the campuses you serve. They create a member custom field for whatever you write back (for example "WiFi PIN"), with Show to member on if the member should see it. They send you the key and their organisation slug.

Once, at first run. Call GET health/ to confirm the key and see which campuses it reaches. Call GET custom-fields/ and store the definition id and name of your field per campus. Call GET members/?status=active,on_notice and page through it to build your initial copy.

Continuously. Have the operator point a webhook at you for member.* and membership.* so joiners, leavers and edits reach you in seconds rather than at your next poll. Keep polling anyway, more slowly: GET members/?updated_since=<your high-water mark> and GET memberships/?updated_since= catch anything a delivery outage lost. Re-check status for anyone whose subscription window you know is closing.

Whenever you provision. PATCH members/<id>/custom-fields/ with {"wifi_pin": "4821"}. The operator's staff now see the value on the member's profile, and the member sees it on their dashboard if the field is shown to them.

When someone stops being entitled. Their status becomes inactive or banned. Withdraw your access. Do not deactivate the member in DeskOS: that is the operator's decision, not yours.


Code examples​

curl​

BASE="https://oscar.nova.deskos.net/api/v1/accounts/external"
KEY="dk_live_xxxxxxxxxxxxxxxxxxxxxxxx"

# 1. What can this key do?
curl -s "$BASE/health/" -H "Authorization: Api-Key $KEY"

# 2. Everyone entitled at campus 7
curl -s "$BASE/members/?campus_id=7&status=active,on_notice&page_size=200" \
-H "Authorization: Api-Key $KEY"

# 3. Is this one person entitled right now?
curl -s "$BASE/members/3f2b6c14-77a1-4d6e-8a19-9c0e7b5a1234/membership/" \
-H "Authorization: Api-Key $KEY"

# 4. Write our PIN back onto them
curl -X PATCH "$BASE/members/3f2b6c14-77a1-4d6e-8a19-9c0e7b5a1234/custom-fields/" \
-H "Authorization: Api-Key $KEY" \
-H "Content-Type: application/json" \
-d '{"wifi_pin": "4821"}'

Python​

import os
import requests

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


def get(path, **params):
r = requests.get(f"{BASE}/{path}", params=params, headers=HEADERS, timeout=30)
data = r.json()
if not data.get("success"):
raise RuntimeError(f"DeskOS refused the request: {data.get('errors') or data.get('detail')}")
return data


def entitled_members(campus_id):
"""Every member who may use the space at this campus right now."""
page = 1
while True:
data = get("members/", campus_id=campus_id, status="active,on_notice",
page=page, page_size=200)
yield from data["members"]
if not data["has_next"]:
return
page += 1


def set_wifi_pin(member_id, pin):
r = requests.patch(f"{BASE}/members/{member_id}/custom-fields/",
json={"wifi_pin": pin}, headers=HEADERS, timeout=30)
data = r.json()
if not data.get("success"):
raise RuntimeError(data.get("errors"))
return data["member"]


for member in entitled_members(campus_id=7):
if not member["custom_fields"].get("wifi_pin"):
set_wifi_pin(member["id"], generate_pin(member["email"]))

JavaScript / Node.js​

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

async function changedSince(isoTimestamp) {
const url = `${BASE}/members/?updated_since=${encodeURIComponent(isoTimestamp)}&page_size=200`;
const res = await fetch(url, { headers });
const data = await res.json();
if (!data.success) throw new Error(JSON.stringify(data.errors || data.detail));
return data.members;
}

async function setCustomField(memberId, name, value) {
const res = await fetch(`${BASE}/members/${memberId}/custom-fields/`, {
method: 'PATCH',
headers,
body: JSON.stringify({ [name]: value }),
});
const data = await res.json();
if (!data.success) throw new Error(JSON.stringify(data.errors));
return data.member;
}

n8n / Zapier / Make​

Use an HTTP Request node. Set the method and URL from the endpoint table, add a header Authorization with value Api-Key dk_live_..., set the body type to JSON, and map your fields. Read success from the response to branch on failure. For a scheduled sync, store the last run's timestamp and pass it as updated_since.


Instructions for AI agents​

The general rules (how a key is obtained, how to handle it, what to do on 401 and 403) are on the Authentication page. Read those first. This section adds what is specific to members.

Permissions to ask for​

Ask only for the members: permissions the task needs. Read permissions are cheap; write permissions are not.

  • "Provision our WiFi for whoever is a live member": members:members:read, members:memberships:read, members:custom_fields:read, members:custom_fields:write. No members:members:write.
  • "Mirror the member directory into our dashboard": members:members:read and members:companies:read only.
  • "Push our HR joiners into DeskOS": members:members:write, plus the two read permissions to check for duplicates first.

If you are asking for members:members:write, say so explicitly to the human and explain what you will create or change. It is the permission that alters an operator's records.

  1. Call GET health/. It confirms the key works and returns scopes, allowed_campuses and default_campus. Use the answer to decide whether you need to send campus_id and whether every operation you plan is in scope, before doing anything else.
  2. Call GET campuses/ and GET custom-fields/ and cache what you get. Custom field definitions are per campus; do not assume a field at one campus exists at another.
  3. If the field you need to write does not exist, stop and ask the human to create it in Memberships, gear icon, on the relevant campus. Do not work around a missing field by writing into a different one.
  4. Page through lists with page_size up to 200 rather than fetching members one at a time.

Behaviour rules​

  • Never treat status as stored state you can set. It is computed from subscriptions. If a member reads as inactive and you think they should be active, the answer is in their company's subscriptions, not in anything you can write.
  • on_notice means entitled. A member serving a notice period may still use the space. Do not cut off their access.
  • Do not deactivate members to reflect your own system's state. Deactivation is the operator's decision about their own member. Withdraw access on your side instead.
  • Check for an existing member before creating one. Search GET members/?email= first. Creating a duplicate splits a person's history in two and the operator has to merge it by hand.
  • A custom field belongs to whoever defined it. Write only the fields your integration owns. Do not overwrite a field the operator's staff maintain, such as an ID number or a vehicle registration, unless the human explicitly asked you to.
  • Respect the daily limit. health/ shows requests_today against daily_limit. The limit is shared by every section the key can reach. Use updated_since for incremental syncs rather than re-reading the whole directory on every run.
  • Treat 404 on a member as "not visible to this key", not proof that they never existed. They may be at a campus the key cannot reach.
  • Do not create test members. Use health/, campuses/ and custom-fields/ to verify connectivity. If you genuinely must create one, name it clearly as a test, tell the human, and deactivate it yourself afterwards.

Reporting back to the human​

When you finish, tell the human:

  • the key name you used (never the key itself),
  • which permissions it turned out to need,
  • how many members and companies you read, and exactly what you created or changed,
  • which custom fields you wrote, on how many members,
  • any 403 or 429 you hit, so they can adjust the key.

Frequently asked questions​

Can I delete a member? No. Deactivate is the strongest write this API exposes. Deletion stays in the admin console, where a human can see what is about to be lost.

Why is a member inactive when their company clearly pays? Status is campus-scoped. A subscription sold at one campus does not entitle a member at another. Call GET members/<id>/membership/?campus_id= at the campus you care about, and check reason: no_active_subscription means the company has nothing live there, while no_company or pending_approval are account problems rather than billing ones.

Does updated_at change when a subscription starts or ends? No. Status is computed, so a subscription change does not touch the member row. Poll GET memberships/?updated_since= alongside GET members/?updated_since=, or re-check status for the members whose windows you know are closing.

Can I set a member's password or send them an invitation? Not through this API. A member created here exists and is approved. To sign them in to your own app, or set their password, use the Member Sign-in API. To have DeskOS send its own invitation, the operator invites them through the admin console.

Can I create the custom field itself through the API? No. Definitions are created by an admin in the console, so a human decides what appears on member profiles. The API reads definitions and writes values.

Can the same key also raise tickets, book rooms or read invoices? Yes, if the admin ticked those permissions on it as well. One key can span several sections. See Service Requests, Meeting Rooms and Billing.

Can I get the invoices for a company I found here? Yes. GET invoices/?company_id= in the Billing API takes the same company id this section returns.

Can DeskOS push changes to me instead of me polling? Yes. An admin can point a URL of yours at member, company and membership events on the Webhooks tab of Developer Settings, and the payload is the same shape this API returns. See Webhooks. Keep a periodic updated_since pass as a floor: a delivery that exhausts its retries is not resent on its own.


Quick reference​

Base:    https://oscar.nova.deskos.net/api/v1/accounts/external/
Auth: Authorization: Api-Key dk_live_...

GET health/
GET campuses/
GET custom-fields/?campus_id=
GET members/?campus_id=&company_id=&status=&email=&updated_since=&page=&page_size=
POST members/ {first_name, last_name, email, company_id, ...}
GET members/<id>/
PATCH members/<id>/ {first_name?, email?, mobile?, company_id?, ...}
POST members/<id>/deactivate/
POST members/<id>/reactivate/
GET members/<id>/membership/?campus_id=
PATCH members/<id>/custom-fields/ {"wifi_pin": "4821"}
GET companies/?campus_id=&status=&name=&updated_since=&page=&page_size=
POST companies/ {name, campus_id?, website?, industry?, address?}
GET companies/<id>/
PATCH companies/<id>/
PATCH companies/<id>/custom-fields/ {"gst_number": "..."}
GET memberships/?campus_id=&company_id=&kind=&status=&updated_since=

Company payload also carries: is_active (boolean), billing (legal name, tax number, address)

Permissions: members:members:read, members:members:write, members:companies:read,
members:companies:write, members:memberships:read,
members:custom_fields:read, members:custom_fields:write,
members:campuses:read
Statuses: active, on_notice, pending, inactive, banned (computed, never stored)
Field types: text, boolean, date, number, select, url

Getting help​

API keys, permissions, campus restrictions, revocation and the access log are all self-service from Developer Settings in your admin console (avatar menu, top right). For a higher daily limit or anything else, reach the DeskOS team through your usual support channel.