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.
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.
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.
| Permission | What it allows | Endpoints |
|---|---|---|
visitors:visitors:read | Read the people on file with their contact details and blacklist status | GET visitors/, GET visitors/<id>/ |
visitors:visits:read | Read visits that happened, and who is on site now | GET visits/, GET visits/<id>/, GET presence/ |
visitors:invitations:read | Read invitations and the people who can be named as a host | GET invitations/, GET invitations/<id>/, GET hosts/ |
visitors:invitations:create | Create an invitation and send it | POST invitations/ |
visitors:invitations:manage | Cancel an invitation, or send an existing one again | POST invitations/<id>/cancel/, POST invitations/<id>/resend/ |
visitors:visits:checkout | Sign a visitor out of the building | POST visits/<id>/checkout/ |
visitors:approvals:read | Read entries waiting on, or settled by, an operator's decision | GET approvals/ |
visitors:setup:read | Read what each campus has switched on | GET settings/ |
visitors:checkins:read | Read the operator's own members coming and going | GET 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.
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:manageandvisitors:visits:checkoutneed Visitors: manage, because each of them changes what happens at the door.visitors:checkins:readneeds 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_idthe key cannot reach answers404rather than an empty list, because an empty list reads as "nobody came". POST invitations/resolves the campus in this order: thecampus_idyou 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 answers400and asks forcampus_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 byemailor onwhatsapp. 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 novisitor_phone, and an email campus rejects one with novisitor_email.require_approval. When this is on, a visitor arriving is held for a decision rather than walking straight in, and the entry appears inapprovals/.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: falsemeans 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.
| Parameter | Notes |
|---|---|
campus_id | Narrow to one campus |
search | Name, email, phone or company |
is_blacklisted | true or false |
date_from, date_to | YYYY-MM-DD, on when the record was first created. Defaults to the last 30 days |
page, page_size | Up 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.
| Parameter | Notes |
|---|---|
campus_id | Narrow to one campus |
date_from, date_to | YYYY-MM-DD, on the expected arrival. With neither, date_from is 30 days ago and the future is left open |
status | Comma separated: pending, confirmed, cancelled, completed, no_show |
purpose | Comma separated: enquiry, meeting, other |
host_id | DeskOS user UUID |
code | The invitation code, case insensitive |
is_walk_in | true or false |
search | Invitee name, email, phone or company |
page, page_size | Up 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
| Field | Required | Notes |
|---|---|---|
visitor_name | yes | The guest's name |
expected_from | yes | ISO 8601 datetime. When the visitor is expected |
host_id or host_email | yes, one of them | The person being visited. Must be a user in your organisation |
campus_id | see Campuses | Integer |
visitor_email | see notes | Required at a campus that invites by email |
visitor_phone | see notes | Required at a campus that invites on WhatsApp |
visitor_company | no | Free text |
purpose | no | enquiry, meeting or other. Defaults to meeting |
expected_until | no | ISO 8601 datetime. Defaults from the arrival window |
arrival_window | no | 15min, 30min, 1hour, 2hours, today, tomorrow, custom. Defaults to 1hour |
notes | no | Free 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.
| Parameter | Notes |
|---|---|
campus_id | Narrow to one campus |
date_from, date_to | YYYY-MM-DD, on check-in. Defaults to the last 30 days |
status | Comma separated: checked_in, checked_out, no_checkout, cancelled |
purpose | Comma separated: enquiry, meeting, other |
host_id | DeskOS user UUID |
visitor_id, invitation_id | Integers |
on_site | true for people currently in the building |
search | Visitor name, email, phone or company |
page, page_size | Up 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.
| Parameter | Notes |
|---|---|
campus_id | Narrow to one campus |
date_from, date_to | YYYY-MM-DD, on when the approval was requested. Defaults to the last 30 days |
status | Comma separated: pending, approved, rejected |
type | Comma separated: invitation, walk_in |
host_id | DeskOS user UUID |
page, page_size | Up 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.
| Parameter | Notes |
|---|---|
role | org for the operator's staff, user for members, all for both. Defaults to org |
campus_id | Narrow to one campus |
search | First name, last name or email |
page, page_size | Up 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.
| Parameter | Notes |
|---|---|
campus_id | Narrow to one campus |
member_id | DeskOS user UUID |
company_id | Integer |
on_site | true for members currently in the building |
date_from, date_to | YYYY-MM-DD, on check-in. Defaults to the last 30 days |
page, page_size | Up 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, plusvisitors:checkins:readif members count as being in the building for this question. - "Invite my guest":
visitors:invitations:create, andvisitors:invitations:readas well so you can list hosts and read back what you created. - "Our gates know when people leave":
visitors:visits:checkoutandvisitors:visits:read. - "How many people came last month?":
visitors:visits:readonly.
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. Checkinvite_channelrather than guessing from the country. - Read
sent_viaon 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
membersisnull, say the key cannot see members rather than reporting the visitor count as the building total. - Never report a
nullas a zero, onmembers, onduration_minutesor 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/showsrequests_todayagainstdaily_limit, shared across every section the key can reach. Usepage_size=200and 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.
- Email: sales@deskos.net
- Phone: +91 742810 9069