Integrating your product with DeskOS
This is the page to send a vendor. It is the whole path from "our customer uses DeskOS" to a working two-way integration, in order, with the decisions already made.
It is written for the products that sit next to a coworking operator and need to know who their members are: WiFi and captive portals, access control, printing, lockers, parking, community apps, HR and CRM sync. If you have integrated with another coworking platform before, this will look familiar. The differences are noted where they exist.
Authentication for the key and the rules every section shares, Members API for the endpoints, and Webhooks for the live feed. This page is the sequence, not a replacement for them.
What you are asking the operator for
An operator sets you up entirely themselves, in Developer Settings (avatar menu, top right of their admin console). Nothing needs DeskOS support, and there is no OAuth application to register, no client id, and no approval queue. Send them this list:
| You need | Where they get it | Notes |
|---|---|---|
| An API key | Developer Settings, API keys tab, "Create API key" | Starts dk_live_. Shown once. |
| The permissions on that key | Ticked when the key is created | Ask for the smallest set. See below. |
| Their campuses | Ticked when the key is created | Restrict the key to the buildings you actually serve. |
| Their organisation slug | Shown on the same page | The acme in acme.deskos.net. Use it to label the connection on your side. |
| A webhook pointing at you | Developer Settings, Webhooks tab | Optional but strongly recommended. Gives you a signing secret, shown once. |
| A custom field to write into | Memberships, gear icon, on each campus | Only if you write a value back, for example a WiFi PIN. |
The key plays the role a client secret plays elsewhere: it identifies your integration and carries its permissions. There is no separate client id, and no token exchange.
A key can only be given permissions the admin creating it holds themselves, and every call it makes is recorded in an access log they can read. Asking for write access you do not use is the fastest way to have an operator refuse the integration.
The permission set for a typical vendor
A "provision our service for whoever is a live member" integration needs exactly four:
members:members:read
members:memberships:read
members:custom_fields:read
members:custom_fields:write
It does not need members:members:write. You are not the system of record for who is a member. If your product also needs to create members (an HR sync, a directory push), that is a different conversation with the operator and a different key.
Day one: prove the connection
Before you write any sync logic, four calls in order. Each one fails differently, so doing them in order tells you which thing is wrong.
BASE="https://oscar.nova.deskos.net/api/v1/accounts/external"
KEY="dk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
# 1. Does the key work, and what can it reach?
curl -s "$BASE/health/" -H "Authorization: Api-Key $KEY"
health/ needs no permission and is your smoke test. It returns the key's scopes, its allowed_campuses, its default_campus and its requests_today against daily_limit. Check the scopes you were promised are actually there before you go further: a missing scope found here is a two-minute fix for the operator, and found in production is an outage.
# 2. Which buildings does this key see?
curl -s "$BASE/campuses/" -H "Authorization: Api-Key $KEY"
# 3. Which custom fields exist, and what are their keys?
curl -s "$BASE/custom-fields/?campus_id=7" -H "Authorization: Api-Key $KEY"
# 4. A first page of members
curl -s "$BASE/members/?campus_id=7&status=active,on_notice&page_size=50" \
-H "Authorization: Api-Key $KEY"
Custom field definitions are per campus. A field called "WiFi PIN" existing at one campus tells you nothing about another. Store the definition id and key per campus at first run, and re-read them periodically. If the field you need is missing, stop and ask the operator to create it. Writing into a different field because yours does not exist corrupts data a human maintains.
If a key restricted to one campus is in use, you can leave campus_id off and DeskOS will use the key's default. Send it explicitly anyway. It makes your logs readable and it does not change behaviour when the operator later adds a second building.
Membership status: the one concept to get right
status on a member is computed, never stored. You cannot set it, and no field anywhere holds it. It is derived from the subscriptions of the member's company at the campus you asked about.
| Status | Entitled? | What it means |
|---|---|---|
active | Yes | A live subscription covers them today. |
on_notice | Yes | Notice has been served, but the period has not ended. They may still use the space. |
pending | No | Account exists, not yet approved. |
inactive | No | No live subscription at this campus. |
banned | No | Deliberately barred by the operator. |
Two mistakes cost vendors their first support ticket:
Cutting off on_notice members. They are paying customers until their last day. Treat active and on_notice identically for access.
Treating status as global. It is per campus. A company with a subscription in Building A does not entitle its member in Building B. Always ask at the campus you are provisioning for: GET members/<id>/membership/?campus_id=7. The reason field tells you which kind of "no" you got: no_active_subscription is a billing answer, no_company and pending_approval are account answers.
Stay in sync
You have two mechanisms and you want both.
Webhooks for speed
Ask the operator to add a webhook on the Webhooks tab pointing at your https endpoint, ticking:
member.created member.updated member.deactivated
membership.activated membership.on_notice membership.ended
Add company.* only if you model companies. member.deleted is rare and carries only identifiers.
The payload's data is the same shape as the Members API response, so the parser you wrote for your initial sync handles the live feed too. Verify X-DeskOS-Signature against the raw body on every request, reply 2xx within ten seconds, queue the work, and deduplicate on the delivery id. The Webhooks page has working receivers in Python, Node and PHP.
membership.activated arrives once per member of the company, not once per subscription. That is deliberate: you provision people, so you should not have to expand a company into its members yourself.
Polling as the floor
Webhooks can be missed. An endpoint that was down through its retries loses that delivery, and a replay is a manual action on the operator's side. So keep a reconciliation pass:
GET members/?updated_since=<your high-water mark>&page_size=200
GET memberships/?updated_since=<your high-water mark>&page_size=200
Nightly is plenty once webhooks are live. Note that updated_at on a member does not move when a subscription starts or ends, because status is computed rather than written: poll memberships/ alongside members/, or re-check status for the members whose windows you know are closing.
Write your value back
The thing that makes an integration feel native to the operator's staff is seeing your data on the member's profile in DeskOS, rather than having to open your dashboard.
Ask the operator to create a member custom field on each campus you serve, with Show to member on if the member should see the value on their own dashboard. Then:
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"}'
Company custom fields mirror member custom fields exactly, at PATCH companies/<id>/custom-fields/, if what you provision belongs to the company rather than the person.
Write only the fields your integration owns. Operators keep their own fields here: ID numbers, vehicle registrations, dietary notes. Overwriting one is not recoverable through the API.
Things you must not do
These are the rules that cause an operator to pull a key, so they are worth reading twice.
- Never deactivate a member to reflect your own system's state. Deactivation is the operator's decision about their own member. If someone should lose your service, withdraw it on your side.
POST members/<id>/deactivate/exists for HR-style directory syncs the operator explicitly asked for, not for provisioning tools. - You cannot delete anything. There is no delete endpoint for a member or a company, and there never will be. A bug in your integration cannot destroy an operator's records.
- Do not create duplicate members. Search
GET members/?email=before creating. A duplicate splits a person's history and a human has to merge it. - Do not create test members in a live workspace.
health/,campuses/andcustom-fields/prove connectivity without touching member data. - Treat a 404 on a member as "not visible to this key", not as proof they do not exist. They may be at a campus outside the key's campus restriction.
- Respect the daily limit. It is shared across every section the key can reach. Incremental syncs with
updated_sincecost a fraction of re-reading the directory.
Going live checklist
Run through this with the operator before you call an integration done.
-
GET health/returns exactly the scopes and campuses you expect. - Every campus you serve has the custom field you write into, and you read its key per campus rather than hardcoding it.
- Your initial sync pages through
members/withpage_sizeup to 200, not one member at a time. -
on_noticemembers are provisioned exactly likeactiveones. - Status is read per campus, never cached globally.
- Your webhook endpoint verifies the signature against the raw body and rejects a mismatch.
- Your webhook handler replies
2xxin well under ten seconds and does its work asynchronously. - Duplicate deliveries are a no-op, keyed on the delivery
id. - A nightly
updated_sincereconciliation runs regardless of webhooks. - You have pressed Send a test delivery and seen the
pingarrive with a valid signature. - Nothing logs the API key or the signing secret.
- The operator knows which fields you write and which they should not touch.
When something breaks
The operator can see more than you can, and they can fix most of it themselves.
| Symptom | Where to look |
|---|---|
401 on every call | The key was revoked or mistyped. Developer Settings, API keys tab shows whether it is still active. |
403 missing scope | The key does not carry that permission. The operator edits the key and ticks it. |
403 organisation suspended | A billing matter between the operator and DeskOS. Nothing you can do. |
429 | Daily limit. Move to incremental syncs, or ask the operator to request a higher limit. |
404 on a member you can see in the UI | Campus restriction on the key. |
| Webhooks stopped arriving | The endpoint auto-disabled after 15 consecutive failures. The Webhooks tab says when and why, and the deliveries log has your responses. |
| A delivery was missed | Replay it from the deliveries log, within the 30 day retention window. |
The Access log tab in Developer Settings shows every call your key made, with method, path, status and timestamp. When you and the operator disagree about what your integration did, that log settles it.
Getting help
Keys, permissions, campus restrictions, webhooks and both logs are entirely self-service for the operator. For a higher daily limit, a listing on the DeskOS integrations page, or anything else, reach the DeskOS team.
- Email: sales@deskos.net
- Phone: +91 742810 9069