Skip to main content

Visitors API

This section of the DeskOS API covers who was invited, who actually turned up, who is in the building right now, and the people on file behind all of it. From an outside system such as a tenant's own booking tool, a building app, a landlord's security desk, a turnstile integration, n8n or Zapier, or an AI agent you can pull the day's expected arrivals, invite somebody yourself, list open visits, sign a visitor out and read a live presence count.

The typical use is a tenant tool that invites its own guests. A company's internal system creates the invitation through this API, the visitor gets the same email or WhatsApp message they would have got from the DeskOS console, the same kiosk pass, and the same door enrolment, and reception sees the guest on the expected list without anybody retyping a name.

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

Creating an invitation sends a real message to a real stranger

POST invitations/ is not a draft. It creates the visitor record, issues the kiosk pass, enrols the visitor on the door gateway where the campus has one, and sends the invitation on that campus's channel immediately. There is no unsending it, no send: false and no preview mode. That is why creating carries its own permission, separate from reading, and why an admin needs Visitors: manage to grant it. Test against a campus and an address you control.


Base path​

All paths on this page are relative to:

https://oscar.nova.deskos.net/api/v1/visitor-management/external/

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


Permissions​

Visitor permissions all start with visitors:. An admin ticks them per key on the Developer Settings page.

PermissionWhat it allowsEndpoints
visitors:visitors:readRead the people on file with their contact details and blacklist statusGET visitors/, GET visitors/<id>/
visitors:visits:readRead visits that happened, and who is on site nowGET visits/, GET visits/<id>/, GET presence/
visitors:invitations:readRead invitations and the people who can be named as a hostGET invitations/, GET invitations/<id>/, GET hosts/
visitors:invitations:createCreate an invitation and send itPOST invitations/
visitors:invitations:manageCancel an invitation, or send an existing one againPOST invitations/<id>/cancel/, POST invitations/<id>/resend/
visitors:visits:checkoutSign a visitor out of the buildingPOST visits/<id>/checkout/
visitors:approvals:readRead entries waiting on, or settled by, an operator's decisionGET approvals/
visitors:setup:readRead what each campus has switched onGET settings/
visitors:checkins:readRead the operator's own members coming and goingGET checkins/, and the member half of GET presence/

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

Four of these need more than Visitors: view

The section itself needs Visitors: view. Four lines need more, and are greyed out on their own if the admin creating the key does not hold it.

  • visitors:invitations:create, visitors:invitations:manage and visitors:visits:checkout need Visitors: manage, because each of them changes what happens at the door.
  • visitors:checkins:read needs Check-ins: view, because a member is not a visitor and a visitor key has no business listing the operator's own members.

An admin with Visitors: view alone still grants every read permission in this section normally.

A tenant booking tool normally wants visitors:invitations:read and visitors:invitations:create. A security desk dashboard wants visitors:visits:read and visitors:checkins:read. A turnstile integration that knows when somebody left wants visitors:visits:checkout on top of visitors:visits:read.


Campuses​

Everything in this section belongs to a campus. The general campus rules are on the Authentication page. For this section:

  • Every list endpoint accepts ?campus_id= to narrow results. Without it they return everything the key can reach.
  • A key restricted to campuses sees visitors, invitations, visits, approvals and settings for those campuses only, including on the detail routes. A visit at a campus your key cannot reach answers 404, not an empty payload.
  • Asking for a campus_id the key cannot reach answers 404 rather than an empty list, because an empty list reads as "nobody came".
  • POST invitations/ resolves the campus in this order: the campus_id you send, then the key's default campus, then the organisation's only campus if it has exactly one. If none of those settles it, the call answers 400 and asks for campus_id. It never guesses between two campuses.

The four things people get wrong​

All four are worth reading before you build anything.

An invitation is not a visit​

An invitation is what was meant to happen. A visit is what did. They are separate records with separate endpoints and they are never folded into one row, because a visit with no invitation is a walk-in and an invitation with no visit is a no-show, and both of those are facts somebody needs to see.

Counting invitations/ tells you how many people were expected. Counting visits/ tells you how many turned up. If you want footfall, use visits. If you want tomorrow's front-desk list, use invitations.

Presence is two populations, never one number​

GET presence/ returns visitors and members separately and never adds them together, because they are not the same thing to a receptionist or a fire warden. Visitors come from open visits. Members come from open check-ins.

The member half needs visitors:checkins:read. Without that permission, members comes back as null, which says the key cannot see them. It is never 0, because zero would claim the building is empty.

The kiosk pass never leaves​

The QR a visitor shows at reception is what opens the turnstile, so it appears in no payload on this page, ever. The invitation code is here because the operator quotes it and the visitor already has it, but the pass behind it is not exposed and cannot be fetched.

If your integration needs to get a pass to somebody, resend the invitation. That sends the message the visitor already expected, through the same path, with the same pass.

Nothing here approves an entry​

GET approvals/ shows who is waiting on a decision and who decided what. There is no endpoint that makes the decision. Letting an outsider into a building is a person's call, taken by somebody who can see them, so it stays in the console and the kiosk.

The same goes for blacklisting. You can read is_blacklisted on a visitor; you cannot set it.


Read the settings first​

GET settings/ tells you what each campus has switched on, and it changes what your integration should expect after it creates an invitation.

  • invite_channel. The campus decides, once, whether invitations go out by email or on whatsapp. A host never picks per invite. This is the single most useful field here, because it decides which contact detail an invitation cannot be created without: a WhatsApp campus rejects a create with no visitor_phone, and an email campus rejects one with no visitor_email.
  • require_approval. When this is on, a visitor arriving is held for a decision rather than walking straight in, and the entry appears in approvals/.
  • auto_approve_returning. Somebody who has been before may be let through without a fresh decision.
  • require_coworking_admin_approval. The operator, not just the host, has to approve.
  • configured: false means the campus has never opened the visitor settings screen. The defaults shown alongside it are what the campus is actually behaving as. This endpoint never creates a settings row.

Endpoints​

Health​

GET health/

Needs no permission. Returns the key's name, organisation, permissions, campus restrictions and today's usage.


Visitors​

GET visitors/

The people on file at this operator. One row per person, not per visit.

ParameterNotes
campus_idNarrow to one campus
searchName, email, phone or company
is_blacklistedtrue or false
date_from, date_toYYYY-MM-DD, on when the record was first created. Defaults to the last 30 days
page, page_sizeUp to 200 per page
{
"success": true,
"count": 412,
"page": 1,
"page_size": 50,
"has_next": true,
"period": { "from": "2026-08-20", "to": null },
"visitors": [
{
"id": 8841,
"name": "Priya Sharma",
"email": "priya@tenant.example",
"phone": "+919000000000",
"company_name": "Tenant Pvt Ltd",
"photo_url": "https://…/visitor-photos/8841.jpg",
"is_blacklisted": false,
"blacklist_reason": null,
"campus": { "id": 7, "name": "Acme North" },
"first_seen_at": "2026-05-02T10:14:00+05:30",
"updated_at": "2026-09-16T09:02:00+05:30"
}
]
}

A detail that was never collected is null, never "". An integration that sees an empty string cannot tell a missing phone number from a blank one. One that sees null can.

blacklist_reason is only sent while the visitor is actually blacklisted. Carrying a stale reason for somebody who has since been cleared would read, to anything displaying it, as though they were still barred.


One visitor​

GET visitors/<id>/

The same record plus visit_count and recent_visits, the last 20 newest first, so "have they been here before" is answered without a second call.


Invitations​

GET invitations/

Who has been invited. A call with no dates covers the last 30 days and everything still to come, because the question people actually ask of this list is who is expected.

ParameterNotes
campus_idNarrow to one campus
date_from, date_toYYYY-MM-DD, on the expected arrival. With neither, date_from is 30 days ago and the future is left open
statusComma separated: pending, confirmed, cancelled, completed, no_show
purposeComma separated: enquiry, meeting, other
host_idDeskOS user UUID
codeThe invitation code, case insensitive
is_walk_intrue or false
searchInvitee name, email, phone or company
page, page_sizeUp to 200 per page
{
"success": true,
"count": 36,
"page": 1,
"page_size": 50,
"has_next": false,
"period": { "from": "2026-08-20", "to": null },
"invitations": [
{
"id": 5512,
"code": "VIS-8KQ2M",
"status": "confirmed",
"purpose": "meeting",
"campus": { "id": 7, "name": "Acme North" },
"host": { "id": "9d1e…", "name": "Ravi Kumar", "email": "ravi@acme.example" },
"visitor": {
"name": "Priya Sharma",
"email": "priya@tenant.example",
"phone": "+919000000000",
"company_name": "Tenant Pvt Ltd"
},
"expected_from": "2026-09-19T15:00:00+05:30",
"expected_until": "2026-09-19T16:00:00+05:30",
"arrival_window": "1hour",
"is_walk_in": false,
"notes": "Bringing a laptop and a monitor.",
"guest_count": 1,
"created_at": "2026-09-18T11:20:00+05:30",
"updated_at": "2026-09-18T11:20:00+05:30"
}
]
}

status means what it says: completed is at least one person turned up, no_show is nobody did, cancelled is somebody called it off. is_walk_in: true marks an invitation created at the desk for somebody already standing there.


Invite somebody​

POST invitations/

Needs visitors:invitations:create. This sends a real message. Read the warning at the top of this page first.

Request headers​

Content-Type: application/json
Authorization: Api-Key dk_live_your_key_here

Minimal request body​

{
"host_email": "ravi@acme.example",
"visitor_name": "Priya Sharma",
"visitor_email": "priya@tenant.example",
"expected_from": "2026-09-19T15:00:00+05:30"
}

This works when the key has a default campus or the organisation has a single campus, and when that campus invites by email.

Full request body​

{
"campus_id": 7,
"host_id": "9d1e5b2c-0a44-4f0e-9d0a-1c2b3a4d5e6f",
"visitor_name": "Priya Sharma",
"visitor_email": "priya@tenant.example",
"visitor_phone": "+919000000000",
"visitor_company": "Tenant Pvt Ltd",
"purpose": "meeting",
"expected_from": "2026-09-19T15:00:00+05:30",
"expected_until": "2026-09-19T17:00:00+05:30",
"arrival_window": "2hours",
"notes": "Bringing a laptop and a monitor."
}

Field reference​

FieldRequiredNotes
visitor_nameyesThe guest's name
expected_fromyesISO 8601 datetime. When the visitor is expected
host_id or host_emailyes, one of themThe person being visited. Must be a user in your organisation
campus_idsee CampusesInteger
visitor_emailsee notesRequired at a campus that invites by email
visitor_phonesee notesRequired at a campus that invites on WhatsApp
visitor_companynoFree text
purposenoenquiry, meeting or other. Defaults to meeting
expected_untilnoISO 8601 datetime. Defaults from the arrival window
arrival_windowno15min, 30min, 1hour, 2hours, today, tomorrow, custom. Defaults to 1hour
notesnoFree text, up to 5000 characters

The host is matched by id or email and nothing else. Matching on a name is deliberately not offered: two people called Priya in the same organisation would make the invitation a coin toss, and the wrong person gets the arrival notification for a guest they have never met.

expected_until is computed when you leave it out, from the arrival window, using the same calculation the invitation message and the pass expiry use. All three then agree on when the visitor stops being expected.

Success response: 201 Created​

{
"success": true,
"invitation": { "id": 5513, "code": "VIS-3RT9P", "status": "confirmed", "…": "…" },
"sent_via": ["email"],
"detail": "Invitation sent by email."
}

Check sent_via. It is the list of channels the message actually went out on, and it can be empty. An invitation whose message could not be sent is still a valid invitation and is still created, because taking the whole request down over a notification failure would be the worse outcome. detail says what happened in one line an operator can read.


One invitation​

GET invitations/<id>/

The invitation with additional_invitees, the extra people on a group booking, and visits, the arrivals recorded against it. An invitation with an empty visits list on a date that has passed is a no-show.


Cancel an invitation​

POST invitations/<id>/cancel/

Needs visitors:invitations:manage. Sets the status to cancelled and takes the visitor back off the door gateway, because an invitation that has been called off should not still open a turnstile and nothing else in the flow would ever revoke it: a no-show never checks out.

Only a pending or confirmed invitation can be cancelled. Anything else answers 400 saying which status blocked it.


Resend an invitation​

POST invitations/<id>/resend/

Needs visitors:invitations:manage. Sends the invitation again on the campus's channel, with the pass the visitor already has.

{ "success": true, "sent_via": ["whatsapp"], "detail": "Invitation sent by WhatsApp." }

This answers 400 when nothing could be sent, rather than reporting success for a message that never left. An operator who is told the pass was resent stops chasing it, so a resend with no reachable contact detail has to fail loudly.


Visits​

GET visits/

People who actually turned up.

ParameterNotes
campus_idNarrow to one campus
date_from, date_toYYYY-MM-DD, on check-in. Defaults to the last 30 days
statusComma separated: checked_in, checked_out, no_checkout, cancelled
purposeComma separated: enquiry, meeting, other
host_idDeskOS user UUID
visitor_id, invitation_idIntegers
on_sitetrue for people currently in the building
searchVisitor name, email, phone or company
page, page_sizeUp to 200 per page
{
"success": true,
"count": 1184,
"page": 1,
"page_size": 50,
"has_next": true,
"period": { "from": "2026-08-20", "to": null },
"visits": [
{
"id": 90211,
"status": "checked_out",
"purpose": "meeting",
"campus": { "id": 7, "name": "Acme North" },
"host": { "id": "9d1e…", "name": "Ravi Kumar", "email": "ravi@acme.example" },
"visitor": {
"id": 8841,
"name": "Priya Sharma",
"email": "priya@tenant.example",
"phone": "+919000000000",
"company_name": "Tenant Pvt Ltd",
"is_blacklisted": false
},
"checked_in_at": "2026-09-18T15:04:00+05:30",
"checked_out_at": "2026-09-18T16:41:00+05:30",
"duration_minutes": 97,
"on_site": false,
"invitation": { "id": 5512, "code": "VIS-8KQ2M" },
"notes": null
}
]
}

duration_minutes is null while the visitor is still on site. A figure computed against "now" would grow every time it was read, which makes it useless to anything storing the number. Work it out yourself from checked_in_at if you need a running figure on a screen.

A visit with "invitation": null is a walk-in.


One visit​

GET visits/<id>/

The same row plus visitor_photo_url when a photo was taken at the desk.


Check a visitor out​

POST visits/<id>/checkout/

Needs visitors:visits:checkout. The one write on the visit side, and it exists for the gate that forgets: a turnstile integration knows the visitor left, and without this the visit sits open until somebody at reception clears it by hand. Checking out also takes the visitor off the door gateway.

{ "checked_out_at": "2026-09-18T16:41:00+05:30" }

checked_out_at is optional and defaults to now. It is refused if it is before the check-in or in the future. A visit that is not on site, or has already been checked out, answers 400.


Presence​

GET presence/

Who is in the building right now. Takes campus_id.

{
"success": true,
"as_of": "2026-09-19T11:02:41+05:30",
"campus_ids": [7, 8],
"visitors": {
"count": 14,
"listed": 14,
"truncated": false,
"visits": []
},
"members": {
"count": 212,
"listed": 200,
"truncated": true,
"checkins": []
}
}

count is always the true figure. listed is how many rows came back, capped at 200, and truncated says whether there are more. A busy floor reports 340 people on site and says it listed the first 200; it never reports 200.

members is null for a key without visitors:checkins:read. That means the key cannot see them, not that there are none.


Approvals​

GET approvals/

Entries waiting on, or settled by, an operator's judgement. Read only.

ParameterNotes
campus_idNarrow to one campus
date_from, date_toYYYY-MM-DD, on when the approval was requested. Defaults to the last 30 days
statusComma separated: pending, approved, rejected
typeComma separated: invitation, walk_in
host_idDeskOS user UUID
page, page_sizeUp to 200 per page

Each row carries the visitor, the host, the invitation it came from where there was one, and decided_by, decided_at and rejection_reason once somebody has ruled on it.


Hosts​

GET hosts/

The people an invitation can name as the host. Staff by default.

ParameterNotes
roleorg for the operator's staff, user for members, all for both. Defaults to org
campus_idNarrow to one campus
searchFirst name, last name or email
page, page_sizeUp to 200 per page

At a coworking site the person being visited is usually a member rather than the operator's own team, so a tenant tool almost always wants ?role=user.


Settings​

GET settings/

What each campus has switched on. Takes campus_id. See Read the settings first.


Member check-ins​

GET checkins/

Needs visitors:checkins:read. The operator's own members coming and going. A member is not a visitor, which is why this carries its own permission and why an operator without Check-ins: view cannot grant it.

ParameterNotes
campus_idNarrow to one campus
member_idDeskOS user UUID
company_idInteger
on_sitetrue for members currently in the building
date_from, date_toYYYY-MM-DD, on check-in. Defaults to the last 30 days
page, page_sizeUp to 200 per page

Each row carries the member, their company, the space and seat where there is one, both timestamps and duration_minutes, which is null while they are still in.


Dates and windows​

Every list that covers a period takes date_from and date_to as YYYY-MM-DD. Give neither and the window is the last 30 days, so an unfiltered call is bounded rather than being a full-table read dressed up as a page of fifty. Give one and the other end stays open, which is how invitations/?date_from=2026-09-19 asks for everything from today onwards including arrivals that have not happened yet.

date_from after date_to answers 400.

The window is applied in your workspace's own timezone, so an evening visit lands on the evening's date rather than the next morning's. Timestamps like checked_in_at, expected_from and as_of are full ISO 8601 with the workspace's offset.


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

Permissions to ask for​

  • "Who is expected today?": visitors:invitations:read.
  • "Who is in the building?": visitors:visits:read, plus visitors:checkins:read if members count as being in the building for this question.
  • "Invite my guest": visitors:invitations:create, and visitors:invitations:read as well so you can list hosts and read back what you created.
  • "Our gates know when people leave": visitors:visits:checkout and visitors:visits:read.
  • "How many people came last month?": visitors:visits:read only.

There is no permission that approves an entry, blacklists a visitor, or checks somebody in. If a human asks for one of those, say that this API cannot do it and that it is done at the kiosk or in the admin console, where the action is attributed to a person who can see the visitor.

Behaviour rules​

  • Never create an invitation without being asked to, and never create one to test something. It sends an email or a WhatsApp message to a real stranger carrying the operator's name, and there is no unsending it.
  • Confirm the host, the campus, the visitor's name and the time back to the human before you POST. Getting the host wrong sends the arrival notification to somebody who has never met the guest.
  • Read settings/ before you invite. A campus on WhatsApp refuses a create with no phone number, and one on email refuses a create with no email address. Check invite_channel rather than guessing from the country.
  • Read sent_via on the response, never assume the message left. An empty list means the invitation exists and nobody was told. Say so.
  • Never add visitors and members into one presence figure. They are separate populations. If members is null, say the key cannot see members rather than reporting the visitor count as the building total.
  • Never report a null as a zero, on members, on duration_minutes or on a contact detail.
  • Do not count invitations/ when you were asked about footfall. Invitations are who was expected; visits/ is who came.
  • Treat a 404 as "not visible to this key", not as proof the thing does not exist. It may belong to a campus the key cannot reach.
  • Never ask for or expose the kiosk pass. If somebody needs their pass again, resend the invitation.
  • Respect the daily limit. health/ shows requests_today against daily_limit, shared across every section the key can reach. Use page_size=200 and a dated window rather than fetching rows one at a time.

Reporting back to the human​

When you finish, tell the human the key name you used (never the key itself), the campuses and date window you covered, and what you actually changed. For an invitation, give the code, the host, the campus, the expected time and the channel it went out on, or say plainly that it went out on nothing. For a check-out, give the visit and the time you recorded. Mention any 403 or 429 you hit, and say if members was null on a presence read.


Frequently asked questions​

How do I get today's expected arrivals? GET invitations/?date_from=<today>&date_to=<today>&status=pending,confirmed.

Why is there no endpoint to check somebody in? Arrival is recorded where the visitor is: at the kiosk, by reception, or by the door gateway. A key cannot see that somebody is standing in the lobby, so it cannot assert that they are. Check-out exists because a gate genuinely does know when somebody left.

I created an invitation and sent_via was empty. Did it work? The invitation exists and the pass was issued, but nothing was sent. Read detail: it usually means the campus invites on WhatsApp and the invitee has no phone number, or the other way round. Fix the detail in the console and resend.

Why can I see visitors but members is null on presence? Your key holds visitors:visits:read and not visitors:checkins:read. The second one lists the operator's own members and needs an admin with Check-ins: view to grant it.

Can I blacklist somebody through the API? No. You can read is_blacklisted and blacklist_reason. Setting them is an operator's decision and stays in the console.

A visit shows "invitation": null. Is that a bug? No, it is a walk-in: somebody who turned up without being invited first.

How do I tell a no-show from a cancellation? The invitation's status. no_show is nobody arrived, cancelled is somebody called it off, completed is at least one person turned up. GET invitations/<id>/ also shows the visits recorded against it.


Quick reference​

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

GET health/
GET visitors/?campus_id=&search=&is_blacklisted=&date_from=&date_to=&page=&page_size=
GET visitors/<id>/
GET invitations/?campus_id=&date_from=&date_to=&status=&purpose=&host_id=&code=
&is_walk_in=&search=&page=&page_size=
POST invitations/ {visitor_name, expected_from, host_id|host_email,
campus_id?, visitor_email?, visitor_phone?,
visitor_company?, purpose?, expected_until?,
arrival_window?, notes?}
GET invitations/<id>/
POST invitations/<id>/cancel/
POST invitations/<id>/resend/
GET visits/?campus_id=&date_from=&date_to=&status=&purpose=&host_id=&visitor_id=
&invitation_id=&on_site=&search=&page=&page_size=
GET visits/<id>/
POST visits/<id>/checkout/ {checked_out_at?}
GET presence/?campus_id=
GET approvals/?campus_id=&date_from=&date_to=&status=&type=&host_id=&page=&page_size=
GET hosts/?role=&campus_id=&search=&page=&page_size=
GET settings/?campus_id=
GET checkins/?campus_id=&member_id=&company_id=&on_site=&date_from=&date_to=
&page=&page_size=

Permissions: visitors:visitors:read, visitors:visits:read, visitors:invitations:read,
visitors:invitations:create (needs Visitors: manage),
visitors:invitations:manage (needs Visitors: manage),
visitors:visits:checkout (needs Visitors: manage),
visitors:approvals:read, visitors:setup:read,
visitors:checkins:read (needs Check-ins: view)
Invitation: pending, confirmed, cancelled, completed, no_show
Visit: checked_in, checked_out, no_checkout, cancelled
Purpose: enquiry, meeting, other
Arrival window: 15min, 30min, 1hour, 2hours, today, tomorrow, custom
Approvals: pending, approved, rejected; type invitation or walk_in
Creating: sends the real message; check sent_via, it can be empty
Presence: two populations, never summed; members null without the permission
Windows: default last 30 days; invitations leave the future open
Never returned: the kiosk QR pass
No endpoint: check-in, approve an entry, blacklist a visitor

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.