This article describes the two endpoints of the Waitlist API and the entry object they return. What the waitlist is, which token you need and how an entry moves from sign-up to booking is in the Waitlist API overview.
| Endpoint | Purpose |
|---|---|
GET /api/v1/waitlist_entries |
List and filter the entries; the interface for syncing. |
GET /api/v1/waitlist_entries/{id} |
Show one entry. |
Both endpoints need an Organization API Token, and read-only tokens may call them. An Instance API Token gets 403 with the code organization_token_required.
The waitlist entry object
| Field | Type | Description |
|---|---|---|
id |
string (uuid) | MeetOne's ID of the entry. Waitlist entries have no external_id: nothing can write one, so store this id in your system. |
status |
string (enum) | waiting, invited, booked, archived or withdrawn (not used). See Statuses. |
first_name, last_name |
string | The person's name. |
email |
string | The address the person confirmed, in lower case. |
phone_number |
string, nullable | The person's phone number, as they entered it. |
date_of_birth |
string (date), nullable | YYYY-MM-DD. |
address_street, address_zipcode, address_place |
string, nullable | The person's address. |
gp_name |
string, nullable | The person's general practitioner. |
message |
string, nullable | What the person wrote in the form's message field. |
internal_note |
string, nullable | Your team's note on the entry. The person never sees it. |
locale |
string, nullable | The language the person signed up in, for example de. |
insurance_type |
string (enum), nullable | gkv (statutory health insurance), pkv (private health insurance) or self_pay. |
preferred_time |
string (enum), nullable | morning, afternoon or all_day. |
consultation_requested |
boolean | Whether the person asked for an initial consultation. |
submitted_at |
string (date-time) | When the person sent the form. The list is in this order. |
email_confirmed_at |
string (date-time), nullable | When the person confirmed their email address. |
consent_accepted_at |
string (date-time), nullable | When the person gave their consent on the form. |
invited_at |
string (date-time), nullable | When the current invitation was sent. null when there is none. |
last_reconfirmed_at |
string (date-time), nullable | When the person last answered the "still interested?" email. |
created_at, updated_at |
string (date-time) | Timestamps. updated_at drives syncing. |
doctor |
object, nullable | The practitioner the person chose in the form: id, external_id, first_name, last_name. null when they chose none. |
invitation |
object, nullable | The invitation to book. See The invitation. |
answers |
array | The answers to your organization's waitlist questions. See Answers. |
Fields the person left empty are null. The links in the emails MeetOne sends to the person are never part of the object.
{
"data": {
"id": "8a3f6d21-4c7e-4b9a-a1d2-6e5f0c9b7d34",
"status": "invited",
"first_name": "Mary",
"last_name": "Major",
"email": "mary.major@example.com",
"phone_number": "+49 30 1234567",
"date_of_birth": "1984-05-17",
"address_street": "Hauptstraße 5",
"address_zipcode": "10115",
"address_place": "Berlin",
"gp_name": null,
"message": "Mornings before ten work best for me.",
"internal_note": null,
"locale": "de",
"insurance_type": "gkv",
"preferred_time": "morning",
"consultation_requested": true,
"submitted_at": "2026-09-14T08:12:44+02:00",
"email_confirmed_at": "2026-09-14T08:15:02+02:00",
"consent_accepted_at": "2026-09-14T08:12:44+02:00",
"invited_at": "2026-10-20T11:30:00+02:00",
"last_reconfirmed_at": null,
"created_at": "2026-09-14T08:12:44+02:00",
"updated_at": "2026-10-20T11:30:00+02:00",
"doctor": {
"id": "b7d2e9c4-1a3f-4c58-8e66-2f9a0d7c5b13",
"external_id": "staff-12",
"first_name": "Maria",
"last_name": "Muster"
},
"invitation": {
"doctor": {
"id": "b7d2e9c4-1a3f-4c58-8e66-2f9a0d7c5b13",
"external_id": "staff-12",
"first_name": "Maria",
"last_name": "Muster"
},
"availability_type": {
"id": "5d9a2f77-c3e1-4b08-8a64-7e1f0c92d3b6",
"external_id": "first-visit",
"display_name": "First visit"
},
"appointment_id": null,
"created_at": "2026-10-20T11:30:00+02:00"
},
"answers": [
{
"question": "Which weekdays suit you?",
"question_type": "multiple_choice",
"text": null,
"number": null,
"selected_options": [
{ "label": "Monday", "text_input": null },
{ "label": "Thursday", "text_input": null }
]
},
{
"question": "How did you hear about us?",
"question_type": "single_choice",
"text": null,
"number": null,
"selected_options": [
{ "label": "Other", "text_input": "A friend" }
]
}
]
}
}
The invitation
invitation is null until your team invites the person, and again after an invitation is withdrawn: when the entry is archived, or when the invitation can no longer be used and the entry returns to waiting. Inviting again replaces it. Once the person has booked, it stays as it was, with the appointment.
| Field | Type | Description |
|---|---|---|
doctor |
object, nullable | The practitioner the invitation is for: id, external_id, first_name, last_name. null when it is for your organization as a whole. |
availability_type |
object, nullable | The availability type the invitation is limited to: id, external_id, display_name. null when the person may choose. |
appointment_id |
string (uuid), nullable | The id of the appointment the person booked through the invitation. Read it with GET /api/v1/appointments/{id}; see Appointments endpoints. null until they book. |
created_at |
string (date-time) | When the invitation was sent. |
Answers
Your organization can add its own questions to the waitlist form. answers holds the person's answers in the order of the questions, and is empty when there are none. A follow-up question that an option opens is an item of its own.
| Field | Type | Description |
|---|---|---|
question |
string | The question as your organization wrote it. |
question_type |
string (enum) | free_text, dropdown, number, single_choice or multiple_choice. |
text |
string, nullable | The answer to a free_text question. |
number |
number, nullable | The answer to a number question. |
selected_options |
array | The options the person picked in a dropdown, single_choice or multiple_choice question: label, and text_input when the option asks for text of its own, such as "Other". Empty for the other types. |
Answers are given in the person's words and are not translated.
List waitlist entries
GET /api/v1/waitlist_entries
| Query parameter | Type | Description |
|---|---|---|
status |
string (enum) | Only entries in this status. Anything other than waiting, invited, booked, archived or withdrawn returns 422 with the code inclusion. |
doctor_id, doctor_external_id |
string | Only entries whose person chose this practitioner. A practitioner who does not exist, or belongs to another organization, returns 404. |
updated_since |
string (date-time) | Only entries changed at or after this time. See below. |
page, per_page |
integer | Pagination. per_page defaults to 50, at most 100. With updated_since, page is an opaque cursor taken from the next link instead of a page number. |
Filters combine: all that you send must match. Without updated_since, the list is ordered by submitted_at, oldest first, which is the order of your waitlist. With updated_since, it is ordered by updated_at.
curl "https://acme.meetone.io/api/v1/waitlist_entries?status=waiting&doctor_external_id=staff-12&per_page=100" \
-H "Authorization: Bearer $TOKEN"
Entries whose email address is not confirmed yet are never listed, and they cannot be asked for: status=unconfirmed is refused like any unknown status.
{
"errors": [
{ "code": "inclusion", "attribute": "status", "message": "Must be one of: waiting, invited, booked, withdrawn, archived." }
]
}
Show a waitlist entry
GET /api/v1/waitlist_entries/{id} returns the entry in the object shown above. It returns 404 if the entry does not exist, belongs to another organization, is not confirmed yet or has been deleted.
Keep your system in sync
updated_since works as on every list of the Scheduling API; Appointments endpoints explains it in detail. In short:
updated_since=<timestamp>returns the entries whoseupdated_atis at or after that time, oldest change first.- It pages by cursor. Follow the
nextURL of thelinkresponse header until there is none, and do not build thepagevalue yourself. - Store entries by
id(upsert). An entry that changes while you page comes again further on. - Next time, send the
updated_atof the last entry you processed, or a minute earlier to be safe.
An entry's updated_at changes with its status, its invitation, an answer to the "still interested?" email and your team's internal note. It is also set when the person confirms their email address, so a new entry reaches your sync with an updated_at later than its submitted_at.
Do not add a status filter to your sync. An entry that leaves that status, for example from invited to booked, no longer matches, and you would never read the change.
Remove deleted entries
A sync never returns a deleted entry: it simply disappears. The Waitlist API overview lists when that happens. To catch deletions, read the whole list from time to time, for example once a day, and remove from your system every entry that was not in it.
Read the whole list with an updated_since far in the past, not with page numbers. Cursor pages continue after the last entry you read, so an entry deleted while you page cannot push another one off its page, as it can with page numbers. The same full read also refreshes what changed without touching updated_at, such as a question your organization renamed.
import requests
from datetime import datetime, timedelta
BASE = "https://acme.meetone.io/api/v1"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
def entries_since(since):
"""Yield every entry changed at or after `since`, page by page."""
url, params = f"{BASE}/waitlist_entries", {"updated_since": since, "per_page": 100}
while url:
response = requests.get(url, headers=HEADERS, params=params)
response.raise_for_status()
yield from response.json()["data"]
# The next URL carries the cursor and every other parameter.
url, params = response.links.get("next", {}).get("url"), None
def sync(last_updated_at):
"""Every few minutes: store what changed; return the new last updated_at."""
since = (datetime.fromisoformat(last_updated_at) - timedelta(minutes=1)).isoformat()
for entry in entries_since(since):
upsert(entry) # idempotent, keyed by entry["id"]
last_updated_at = entry["updated_at"] # oldest first: the last is the newest
return last_updated_at
def reconcile():
"""Once a day, and for the first run: store everything, drop what MeetOne deleted."""
seen, last_updated_at = set(), None
for entry in entries_since("2000-01-01T00:00:00+00:00"):
upsert(entry)
seen.add(entry["id"])
last_updated_at = entry["updated_at"]
delete_all_except(seen) # entries deleted in MeetOne
return last_updated_at
Persist the value sync and reconcile return, and pass it to the next sync. A full read costs one request per 100 entries, so keep the rate limits of your Organization API Token in mind when you choose how often to run it.
Errors at a glance
| Status | Code | Meaning |
|---|---|---|
403 |
organization_token_required |
The request used an Instance API Token. |
403 |
feature_disabled |
The waitlist is not enabled for your organization. |
404 |
not_found |
The entry, or the practitioner you filtered by, does not exist or belongs to another organization. Entries that are not confirmed yet or were deleted return it too. |
422 |
inclusion |
status is not one of the statuses above. |
422 |
ambiguous_reference |
Both doctor_id and doctor_external_id were sent. |
422 |
invalid |
updated_since or the page cursor cannot be read, or per_page is below 1. |
Related
Questions? Contact us at support@meetone.io.