Guests
The guests CRM endpoints expose your operator guest roster: search across everyone who has booked with your venues, drill into a single guest’s history, billing and social network, apply tags, and create lightweight walk-in guests. All guests-CRM routes require the operator role and are scoped to your own account and venues.
Endpoints
Section titled “Endpoints”| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /api/my-guests |
Guest roster (paginated) | operator |
| GET | /api/my-guests/search |
Search the roster | operator |
| GET | /api/my-guests/network |
Full operator guest graph | operator |
| GET | /api/my-guests/:id |
One guest with bookings, stats, tags | operator |
| GET | /api/my-guests/:id/billing |
Guest billing ledger | operator |
| GET | /api/my-guests/:id/activity |
Recent activity (last 10) | operator |
| GET | /api/my-guests/:id/bookings |
All of a guest’s bookings | operator |
| GET | /api/my-guests/:id/network |
1-hop network around a guest | operator |
| PUT | /api/my-guests/:id/tags |
Replace a guest’s tags at a scope | operator |
| GET | /api/my-venues/:venueId/guests/search |
Venue-scoped guest search | operator |
| POST | /api/my-venues/:venueId/guests/walk-in |
Create or match a walk-in guest | operator |
| GET | /api/users/:id/score |
A guest’s Sidedoor score and breakdown | operator |
| GET | /api/my-guests/:id/ask-aimy |
Whether Aimy is available for this guest | operator |
| GET | /api/my-guests/:id/brief |
Compact facts: visits, no-shows, spend, favourite venue | operator |
| GET | /api/my-guests/:id/loyalty-summary |
Compact loyalty badges: score, trust and a recommended action | operator |
| POST | /api/my-guests/merge |
Merge duplicate guest accounts into one | operator |
Search guests
Section titled “Search guests”Roster search matches name, email or phone (minimum 2 characters, capped at 50
results). Use /api/my-guests with pagination to page the whole roster.
curl "https://thesidedoor.co/api/my-guests/search?q=sam" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN"const res = await fetch( "https://thesidedoor.co/api/my-guests/search?q=sam", { headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}` } },);const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-guests/search?q=sam");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],]);$data = json_decode(curl_exec($ch), true);{ "data": [ { "id": "usr_abc", "name": "Sam Lee", "email": "sam@example.com", "phone": "+44...", "membership_tier": "black", "loyalty_points": 240, "successful_visits": 12, "venue_ids": "ven_123,ven_456" } ]}Fetch a guest
Section titled “Fetch a guest”GET /api/my-guests/:id returns the guest together with their recent bookings,
a marketing engagement summary, a products breakdown (reservations, events,
guest list, vouchers), any reviews they have left at your venues, aggregate
stats, and their tags. Tags come in four sets: your manual global and
per_venue tags, read-only auto loyalty tags, and read-only rule_auto tags
applied by a tag rule.
curl "https://thesidedoor.co/api/my-guests/usr_abc" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN"const res = await fetch("https://thesidedoor.co/api/my-guests/usr_abc", { headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}` },});const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-guests/usr_abc");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],]);$data = json_decode(curl_exec($ch), true);{ "guest": { "id": "usr_abc", "name": "Sam Lee", "email": "sam@example.com", "phone": "+44...", "membership_tier": "black", "dob": "1990-04-12" }, "bookings": [ { "id": "bkg_9a1", "status": "checked_in", "guest_count": 4, "booking_date": "2026-06-20", "venue_name": "The Attic" } ], "marketing": { "email_sent": 12, "email_opened": 9, "email_clicked": 3, "email_open_rate": 75, "email_click_rate": 25, "push_enabled": true, "favourite_category": "Cocktail bars" }, "products": { "reservations": { "total": 15, "attended": 12, "cancelled": 1, "no_show": 0, "avg_guests": 3.2 }, "events": { "booked": 4, "attended": 3, "last_at": "2026-05-02T19:00:00Z" }, "guestlist": { "total": 2, "accepted": 2, "declined": 0 }, "vouchers": { "purchased": 3, "redeemed": 2, "outstanding": 1, "gifted": 1 } }, "reviews": { "count": 1, "avg_rating": 5, "items": [ { "rating": 5, "comment": "Great night", "source_type": "post_visit", "venue_id": "ven_123", "venue_name": "The Attic", "created_at": "2026-06-21T10:00:00Z" } ] }, "stats": { "loyalty_points": 240, "loyalty_points_network": 980, "loyalty_tier": "Gold", "loyalty_progress": { "tier": "Gold", "next_tier": "Platinum", "points": 980, "pts_to_next": 220, "pct": 78 }, "confirmed_visits": 12, "door_rejections": 0, "no_shows": 0, "total_bookings": 15, "total_spend_pence": 48200, "spend_breakdown": { "events_pence": 12000, "vouchers_pence": 6000, "deposits_pence": 30200 }, "keys_spent": 30 }, "is_connection": false, "tags": { "global": ["vip"], "per_venue": [{ "venue_id": "ven_123", "venue_name": "The Attic", "tags": ["regular"] }], "auto": [{ "key": "big_spender", "label": "Big spender" }], "rule_auto": [{ "venue_id": "ven_123", "venue_name": "The Attic", "tag_label": "weeknight-regular" }] }}The related reads - GET /api/my-guests/:id/billing, /activity, /bookings
and /network - follow the same auth and path shape:
curl "https://thesidedoor.co/api/my-guests/usr_abc/billing" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN"const res = await fetch( "https://thesidedoor.co/api/my-guests/usr_abc/billing", { headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}` } },);const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-guests/usr_abc/billing");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],]);$data = json_decode(curl_exec($ch), true);{ "data": [ { "id": "led_1", "type": "spend", "delta": -5000, "paid_delta": -5000, "free_delta": 0, "note": "Table deposit", "booking_id": "bkg_9a1", "created_at": "2026-06-20T20:00:00Z" } ]}Each row splits its delta into paid_delta (keys the guest paid for) and
free_delta (promotional keys). type is spend or refund.
Guest brief
Section titled “Guest brief”GET /api/my-guests/:id/brief returns a compact set of facts about a guest at
your venues: visits, no-shows, cancellations, total spend, their favourite
venue, and their first and last visit. It’s the same data shown on the guest
tile’s summary and in the Aimy chat panel - see
Aimy, your portal assistant.
curl "https://thesidedoor.co/api/my-guests/usr_abc/brief" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN"const res = await fetch( "https://thesidedoor.co/api/my-guests/usr_abc/brief", { headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}` } },);const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-guests/usr_abc/brief");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],]);$data = json_decode(curl_exec($ch), true);{ "guest_id": "usr_abc", "brief": { "name": "Sam Lee", "visits": 12, "no_shows": 0, "cancellations": 1, "total_spend_pence": 48200, "favourite_venue": "The Attic", "first_visit": "2025-02-14", "last_visit": "2026-06-20" }, "has_data": true}has_data is false when the guest has no bookings yet at your venues, so
brief is a mostly-empty shell.
Loyalty summary
Section titled “Loyalty summary”GET /api/my-guests/:id/loyalty-summary returns a compact, render-ready set
of loyalty badges for a guest tile: their Sidedoor score, a trust verdict, a
recommended action, their automatic loyalty tags, and whether their birthday
is coming up.
curl "https://thesidedoor.co/api/my-guests/usr_abc/loyalty-summary" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN"const res = await fetch( "https://thesidedoor.co/api/my-guests/usr_abc/loyalty-summary", { headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}` } },);const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-guests/usr_abc/loyalty-summary");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],]);$data = json_decode(curl_exec($ch), true);{ "score": 72, "trust": "trusted", "action": "none", "auto_tags": [{ "key": "big_spender", "label": "Big spender" }], "birthday_near": false}A walk-in guest (never registered, with no reachable contact) has no
meaningful score or tags, and the endpoint returns just { "walk_in": true }
instead.
Merge duplicate guests
Section titled “Merge duplicate guests”POST /api/my-guests/merge collapses a group of duplicate guest accounts -
for example one created by a booking and another imported separately - onto
a single survivor. Pass the primary_id to keep and the full ids array (2
to 50, the primary included); every other account’s bookings, reservations,
vouchers, reviews and roster membership at your venues move onto the primary,
and the duplicates drop off your roster. This only affects your own
operator’s view of the accounts - it never merges or deletes the underlying
guest accounts themselves, which may also belong to other operators.
curl -X POST "https://thesidedoor.co/api/my-guests/merge" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"primary_id":"usr_abc","ids":["usr_abc","usr_def"]}'const res = await fetch("https://thesidedoor.co/api/my-guests/merge", { method: "POST", headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ primary_id: "usr_abc", ids: ["usr_abc", "usr_def"] }),});const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-guests/merge");curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$token}", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "primary_id" => "usr_abc", "ids" => ["usr_abc", "usr_def"], ]),]);$data = json_decode(curl_exec($ch), true);{ "success": true, "primary_id": "usr_abc", "merged": 1 }Every id must already be on your roster, and all accounts you merge must be guests of your own operator - you can’t merge in an account from outside your roster.
Tag a guest
Section titled “Tag a guest”Replace a guest’s tags at a scope. scope is global (default) or venue;
when venue, pass the venue_id. Tags are sent as an array and fully replace
the existing set at that scope.
curl -X PUT "https://thesidedoor.co/api/my-guests/usr_abc/tags" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"scope":"venue","venue_id":"VENUE_ID","tags":["regular","big-spender"]}'const res = await fetch( "https://thesidedoor.co/api/my-guests/usr_abc/tags", { method: "PUT", headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ scope: "venue", venue_id: "VENUE_ID", tags: ["regular", "big-spender"], }), },);const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-guests/usr_abc/tags");curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => "PUT", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$token}", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "scope" => "venue", "venue_id" => "VENUE_ID", "tags" => ["regular", "big-spender"], ]),]);$data = json_decode(curl_exec($ch), true);{ "success": true }Guest network graph
Section titled “Guest network graph”GET /api/my-guests/:id/network returns a one-hop graph centred on a guest: the
centre node plus their direct connections. Each node is { id, name, degree, tier },
and the centre node also carries centre: true. The response wraps nodes, edges
and centre_id.
curl "https://thesidedoor.co/api/my-guests/usr_abc/network" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN"const res = await fetch( "https://thesidedoor.co/api/my-guests/usr_abc/network", { headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}` } },);const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-guests/usr_abc/network");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],]);$data = json_decode(curl_exec($ch), true);{ "nodes": [ { "id": "usr_abc", "name": "Sam Lee", "degree": 3, "tier": "black", "centre": true }, { "id": "usr_def", "name": "Alex Ray", "degree": 5, "tier": "gold" } ], "edges": [{ "source": "usr_abc", "target": "usr_def" }], "centre_id": "usr_abc"}GET /api/my-guests/network returns the whole operator guest graph. Its nodes are
richer, carrying email, phone, account_type, company_name, sidedoor_score,
loyalty_tags and client_tags alongside id, name, tier and degree.
Connected guests who are not your own (they have never booked at your venues) appear
as redacted nodes: { id, name: null, anonymous: true, degree }, with no contact
details or score. This graph returns nodes and edges only (no centre_id).
Venue-scoped search
Section titled “Venue-scoped search”GET /api/my-venues/:venueId/guests/search is a compact search (min 2 chars,
12 results) for picking a guest when creating a booking at a specific venue.
curl "https://thesidedoor.co/api/my-venues/VENUE_ID/guests/search?q=sam" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN"const res = await fetch( "https://thesidedoor.co/api/my-venues/VENUE_ID/guests/search?q=sam", { headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}` } },);const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-venues/VENUE_ID/guests/search?q=sam");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],]);$data = json_decode(curl_exec($ch), true);{ "data": [ { "id": "usr_abc", "name": "Sam Lee", "email": "sam@example.com", "phone": "+44...", "profile_image_url": null, "sidedoor_score": 72, "sidedoor_loyalty_tags": [{ "key": "big_spender", "label": "Big spender" }], "client_tags": ["regular"] } ]}Each row carries the guest’s sidedoor_score, read-only sidedoor_loyalty_tags
and your own manual client_tags (global plus this venue), matching the columns
in the Guests pane.
Create a walk-in guest
Section titled “Create a walk-in guest”Create a lightweight guest for someone booking on the night. Supply at least one
of name, email or phone. If you omit name, the guest is created as
“Walk-in”; a missing email is synthesised. Use the returned id as the user_id
when creating a staff booking.
If the email or phone you send matches a guest already in the system, the endpoint
returns that existing account instead of creating a new one, with existing: true
in the response. A newly created guest omits that field.
curl -X POST "https://thesidedoor.co/api/my-venues/VENUE_ID/guests/walk-in" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"Jordan Fox","phone":"+44..."}'const res = await fetch( "https://thesidedoor.co/api/my-venues/VENUE_ID/guests/walk-in", { method: "POST", headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "Jordan Fox", phone: "+44..." }), },);const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-venues/VENUE_ID/guests/walk-in");curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$token}", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode(["name" => "Jordan Fox", "phone" => "+44..."]),]);$data = json_decode(curl_exec($ch), true);{ "id": "usr_walkin1", "name": "Jordan Fox", "email": "walkin-1a2b3c4d@local", "phone": "+44..." }A matched existing account instead returns existing: true:
{ "id": "usr_abc", "name": "Sam Lee", "email": "sam@example.com", "phone": "+44...", "existing": true }Guest Sidedoor score
Section titled “Guest Sidedoor score”GET /api/users/:id/score returns a guest’s Sidedoor score and its breakdown.
As an operator you can read the score of any guest linked to one of your venues
(by a booking or reservation). The score is not part of the guest-detail response,
it lives on this dedicated endpoint.
curl "https://thesidedoor.co/api/users/usr_abc/score" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN"const res = await fetch("https://thesidedoor.co/api/users/usr_abc/score", { headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}` },});const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/users/usr_abc/score");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],]);$data = json_decode(curl_exec($ch), true);total is the 0-100 headline score and rank its label. parts breaks the raw
total into its four contributors, and loyaltyTags are the automatic loyalty tags
derived from the same signals.
{ "total": 72, "raw": 148, "rank": "Regular", "parts": { "loyalty": 60, "connections": 40, "age": 35, "tier": 13 }, "loyaltyTags": [{ "key": "big_spender", "label": "Big spender" }], "inputs": { "connectionCount": 8, "ageDays": 255, "tier": "black" }, "previous": { "score": 68, "at": "2026-09-11T00:00:00Z" }}previous is the stored baseline from about a week ago (or null if none is
recorded yet), so you can show a trend against the current total.
Check Aimy availability for a guest
Section titled “Check Aimy availability for a guest”GET /api/my-guests/:id/ask-aimy reports whether the Aimy add-on is
enabled on your account, so a client can show or hide an “Ask Aimy about this
guest” entry point for the guest. Asking the actual question happens in the
Aimy chat panel inside the operator portal, not over the API - see
Aimy, your portal assistant.
curl "https://thesidedoor.co/api/my-guests/usr_abc/ask-aimy" \ -H "Authorization: Bearer $SIDEDOOR_TOKEN"const res = await fetch( "https://thesidedoor.co/api/my-guests/usr_abc/ask-aimy", { headers: { Authorization: `Bearer ${process.env.SIDEDOOR_TOKEN}` } },);const data = await res.json();$ch = curl_init("https://thesidedoor.co/api/my-guests/usr_abc/ask-aimy");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$token}"],]);$data = json_decode(curl_exec($ch), true);{ "entitled": true }Without the add-on, this returns { "entitled": false, "addon": "aimy" }
instead of a 403, so you can render a locked state without treating it as an
error.