Getting started
The Parlor API gives software you run access to ONE venue's data in Parlor. Each key belongs to one venue, and everything it reads or writes is that venue's. There is nothing in a request that can point it at another venue.
- The venue's owner asks for access. In Parlor, under Settings → Integrations & API, the owner presses Request API access and says in a paragraph what will be built. Only the owner can do this.
- Parlor reviews the request and replies. Access is for venues on the Pro or Group plan, or on an older Parlor arrangement that already includes Zapier. The owner sees the answer in the same place in Settings.
- Once approved, the owner creates a key in the same place, choosing Read-only or Read and write. The key is shown once. Copy it straight into your server's secret settings.
Then make your first call. Every address starts https://parlorhq.app/api/v1/.
curl https://parlorhq.app/api/v1/me \
-H "X-Parlor-Key: parlor_sk_YOUR_KEY_HERE"
A working key answers with the venue it belongs to, and what kind of key it is (kind is api for your own software, zapier for Parlor’s Zapier app; scope is read or read_write):
{
"venue": {
"slug": "cotillion",
"name": "Cotillion Cafe",
"timezone": "America/New_York"
},
"plan": "pro",
"key": {
"kind": "api",
"scope": "read"
}
}
There is no separate test system. Everything a key does happens in the venue's real Parlor. Start with a Read-only key while you build, and when you move to writing, use a test guest with your own email address and cancel anything you create.
Authentication
Send the key in the X-Parlor-Key header on every request.
- Only in that header. Never in the address (a key in a URL ends up in server logs, browser history and analytics), and never in
Authorization, which Parlor keeps for staff sign-ins.
- Only on a server. Never put a key in a web page, a phone app or anything a guest can download. Anyone who has the key can read the venue's guest list.
- A key looks like
parlor_sk_ followed by 40 letters and digits. Parlor stores only a fingerprint of it, so a lost key cannot be shown again: make a new one and revoke the old one.
- The owner can revoke a key at any time in Settings. It stops working at once, and so do any webhook endpoints it made.
A key is refused with 401 when it is missing, unknown or revoked, and with 403 when the key is fine but the venue cannot use the API right now: its plan no longer includes it, or its account is paused.
If Parlor withdraws the venue's API access, every API key it has is revoked for good at that moment, so from then on they answer 401, like any revoked key. Making a new key will not help until Parlor gives access again: the owner sees in Settings → Integrations & API that access was switched off, and why, and can ask again there.
Read-only and read-write keys
| Key | What it can do |
| Read-only | Every GET: the lists, the searches and /api/v1/me. Anything else is refused with 403. |
| Read and write | Everything a read-only key can do, plus creating guests, subscribers, reservation requests, tasks and enquiries, and managing webhook endpoints. |
A read-only key that tries to change something gets:
{
"error": "This API key is read-only, so it can look things up but can't add, change or remove anything. Create a Read and write key in Parlor under Settings -> Integrations & API if this app needs to make changes."
}
Use the smallest key that does the job. A dashboard that only shows bookings needs a read-only key.
Keys made for Zapier live in their own section of Settings, need no request, and are for Parlor's Zapier app. API keys are separate from them and are only issued once access is approved.
Rate limits
Each key may make 120 requests a minute. Past that, Parlor answers 429 Too Many Requests with a Retry-After header saying how many seconds to wait.
{
"error": "Too many requests with this key -- Parlor allows 120 a minute. Try again in 20 seconds."
}
- Wait the number of seconds in
Retry-After, then carry on. Do not retry in a tight loop.
- Prefer webhooks to polling. A webhook costs you nothing against the limit.
- A request body may be at most 64 KB on the
POSTs that add guests, subscribers, reservation requests, tasks and enquiries (a larger one is refused with 400), and at most 16 KB on POST /api/v1/webhooks (refused with 413).
Errors
Every refusal is JSON with one plain-English sentence in error, written so you can show it to a person as it is. Where a program might want to act on the kind of refusal, there is also a short, stable reason.
{
"error": "The venue is closed that day (Closed for Thanksgiving).",
"reason": "closed"
}
| Status | Means | Try again? |
| 400 | Something in the request is missing or unreadable, or the body is over 64 KB. The sentence says what. | After fixing it |
| 401 | No key, or a key Parlor does not recognise or has revoked. | No |
| 403 | The venue can't use the API right now; a read-only key tried to change something; or a Zapier key or read-only key tried to manage webhooks. | No |
| 404 | No such address, or no such record for this venue. | No |
| 405 | That address does not take that method. | No |
| 409 | The venue said no (fully booked, closed, too late), or an Idempotency-Key clash (see Safe retries). | With different details |
| 413 | The body sent to POST /api/v1/webhooks is over 16 KB. | No |
| 429 | Too many requests this minute. | After Retry-After |
| 500 | Something failed on Parlor's side. | Yes, with a pause |
| 503 | Parlor could not check the key or reach the data just now. | Yes, with a pause |
Lists, limit and since
The list endpoints answer a JSON array, newest first by when each record was created.
limit: how many, 1 to 100. The default is 25.
since: only records created at or after this moment. Give a time zone: 2026-10-01T00:00:00-04:00.
curl "https://parlorhq.app/api/v1/reservations?limit=100&since=2026-10-01T00:00:00-04:00" \
-H "X-Parlor-Key: parlor_sk_YOUR_KEY_HERE"
Lists are for keeping up, not for copying history. A call returns at most 100 records, always the newest, and there is no "next page". To keep a copy in step, ask with since set a few minutes before the newest createdAt you already hold, skip ids you have, and ask often enough that fewer than 100 new records arrive between calls. If a call comes back full, some older ones in that window were not returned: ask sooner next time. For changes to records you already have, use webhooks. To start from a venue's whole history, the owner can ask Parlor for an export.
Dates and times. A date (2026-10-17) and a time (19:30) are always the venue's local time, in the time zone /api/v1/me gives. Moments such as createdAt are full ISO 8601 date-times in UTC, ending Z.
Fields. Every field in an object is always present, and is null when Parlor has no value. New fields may be added; ignore ones you do not know.
Safe retries: Idempotency-Key
If a connection drops after Parlor saved something but before you heard back, a retry could make it twice. To stop that, send an Idempotency-Key header with any unique string you choose (up to 200 characters) on a POST.
- The same key with the same body within 24 hours gets the first answer back, marked with the header
Idempotent-Replayed: true, and nothing is done twice.
- The same key with a different body is refused with
409. That is a bug in the caller, and Parlor will not guess.
- A retry that arrives while the first try is still being worked on is refused with
409: wait a moment and send it again.
- On reservation requests, tasks and enquiries, if an earlier try with the same key started more than a short while ago and never finished, Parlor cannot tell whether it went through, so it refuses with
409 and reason outcome-unknown rather than risk making it twice. Check in Parlor before sending it again with changed details. Guests and subscribers are safe to repeat, so there the retry simply goes ahead.
- Only successful answers are kept. A "fully booked" refusal is not replayed, so a retry later gets a fresh look at the book.
- A kept reservation or enquiry that has since been cancelled or deleted is not replayed: the same details again are a new request.
The objects
Each kind of record has one shape, the same in lists, searches, the replies to actions and webhook deliveries. What never leaves Parlor, whatever you ask for: the private links in guests' emails, staff-written notes and allergies about a guest, which member of staff did something, card and payment ids, and Parlor's internal ids other than a record's own.
Reservation
A booking. The same shape in lists, searches, action replies and webhook deliveries.
id | integer The reservation's id. Stable. |
|---|
guestName | string, or null Name on the booking. |
|---|
email | string, or null Guest email on the booking. |
|---|
phone | string, or null Guest phone on the booking, as typed. |
|---|
partySize | integer, or null Number of guests. |
|---|
date | string, or null Venue-local date, YYYY-MM-DD. |
|---|
time | string, or null Venue-local time, HH:MM (24-hour). |
|---|
durationMinutes | integer, or null How long the table is held. |
|---|
status | string, or null Requested, Confirmed, Seated, Completed, Cancelled or No-show. |
|---|
source | string, or null Where the booking came from: public (the booking page), staff, walkin, waitlist, zapier (an integration or the API), ... |
|---|
bookingType | string, or null The venue's name for the kind of booking ("Dinner", "Afternoon tea"), if any. |
|---|
notes | string, or null The booking's own request ("anniversary, window table"). Never staff notes about the guest. |
|---|
floorStage | string, or null Where the party is in their meal, when the venue tracks it. |
|---|
createdAt | string, or null When the booking was made. |
|---|
checkedInAt | string, or null When the party arrived. |
|---|
cancelledAt | string, or null When it was cancelled. |
|---|
cancelledBy | string: guest, staff, or null Who cancelled it. Never which member of staff. |
|---|
updatedAt | string, or null The latest change Parlor has a time for. In a webhook delivery, when this change happened. |
|---|
Example
{
"id": 48213,
"guestName": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"partySize": 4,
"date": "2026-10-17",
"time": "19:30",
"durationMinutes": 90,
"status": "Confirmed",
"source": "public",
"bookingType": "Dinner",
"notes": "Anniversary -- window table if possible",
"floorStage": null,
"createdAt": "2026-10-02T14:05:11.482Z",
"checkedInAt": null,
"cancelledAt": null,
"cancelledBy": null,
"updatedAt": "2026-10-02T14:05:11.482Z"
}
Guest
A guest record. Staff-written profile notes and allergies are never included.
id | integer The guest's id. Stable. |
|---|
name | string, or null Name. |
|---|
email | string, or null Email address. |
|---|
phone | string, or null Phone number, as typed. |
|---|
seatingPreference | string, or null Preferred seating, if recorded. |
|---|
referralSource | string, or null How they first came to the venue. |
|---|
visitCount | integer, or null Visits recorded. |
|---|
noShowCount | integer, or null No-shows recorded. |
|---|
cancellationCount | integer, or null Cancellations recorded. |
|---|
lastVisitDate | string, or null Venue-local date of the last visit. |
|---|
createdAt | string, or null When the record was made. |
|---|
updatedAt | string, or null When the record last changed. |
|---|
Example
{
"id": 1760,
"name": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"seatingPreference": "Booth",
"referralSource": "Website",
"visitCount": 6,
"noShowCount": 0,
"cancellationCount": 1,
"lastVisitDate": "2026-09-12",
"createdAt": "2025-11-03T17:22:40.000Z",
"updatedAt": "2026-09-12T23:10:02.000Z"
}
Subscriber
Someone on the venue's email list.
id | integer Id. Stable. |
|---|
email | string, or null Email address. |
|---|
firstName | string, or null First name, if given. |
|---|
status | string, or null subscribed, pending (waiting to confirm) or unsubscribed. |
|---|
source | string, or null How they joined (signup_form, booking, zapier, import, ...). |
|---|
birthMonth | integer, or null Birthday month, 1-12, if given. |
|---|
birthDay | integer, or null Birthday day of the month, if given. |
|---|
consentAt | string, or null When they agreed to marketing email. |
|---|
confirmedAt | string, or null When they confirmed, for double opt-in. |
|---|
unsubscribedAt | string, or null When they left the list. |
|---|
createdAt | string, or null When the row was made. |
|---|
updatedAt | string, or null When the row last changed. |
|---|
Example
{
"id": 8812,
"email": "[email protected]",
"firstName": "Jordan",
"status": "subscribed",
"source": "signup_form",
"birthMonth": 4,
"birthDay": 22,
"consentAt": "2026-03-14T18:02:55.000Z",
"confirmedAt": "2026-03-14T18:04:10.000Z",
"unsubscribedAt": null,
"createdAt": "2026-03-14T18:02:55.000Z",
"updatedAt": "2026-03-14T18:04:10.000Z"
}
Ticket Order
A ticket order. Abandoned and in-flight checkouts are never listed. Money is given both in cents and as a decimal string.
id | integer Id. Stable. |
|---|
orderNumber | string, or null The order number the buyer sees. |
|---|
status | string, or null paid, paid_unconfirmed, refunded, partially_refunded, refund_failed or cancelled. |
|---|
channel | string, or null public (bought online) or box_office (sold by staff or at the door). |
|---|
paymentMethod | string, or null square_link, cash, comp, free or external. |
|---|
buyerFirstName | string, or null Buyer's first name. |
|---|
buyerLastName | string, or null Buyer's last name. |
|---|
buyerEmail | string, or null Buyer's email. |
|---|
buyerPhone | string, or null Buyer's phone. |
|---|
eventName | string, or null The event. |
|---|
occurrenceTitle | string, or null Which date or seating of the event, if named. |
|---|
eventDate | string, or null Venue-local date of the event. |
|---|
eventStartTime | string, or null Venue-local start time, HH:MM. |
|---|
subtotalCents | integer, or null Before discounts, fees and tax, in cents. |
|---|
discountCents | integer, or null Discount, in cents. |
|---|
feeCents | integer, or null Fees, in cents. |
|---|
taxCents | integer, or null Tax, in cents. |
|---|
totalCents | integer, or null What the buyer paid, in cents. |
|---|
refundedCents | integer, or null Refunded so far, in cents. |
|---|
total | string, or null totalCents as dollars, e.g. "99.80". |
|---|
refunded | string, or null refundedCents as dollars. |
|---|
promoCode | string, or null Promo code used, if any. |
|---|
marketingOptIn | boolean, or null Whether the buyer ticked the box to hear from the venue. |
|---|
paidAt | string, or null When it was paid. |
|---|
createdAt | string, or null When the order was started. |
|---|
updatedAt | string, or null When it last changed. |
|---|
Example
{
"id": 311,
"orderNumber": "MR-7K4Q2",
"status": "paid",
"channel": "public",
"paymentMethod": "square_link",
"buyerFirstName": "Jordan",
"buyerLastName": "Ellis",
"buyerEmail": "[email protected]",
"buyerPhone": "(555) 010-4477",
"eventName": "Murder Mystery Supper",
"occurrenceTitle": "Friday seating",
"eventDate": "2026-10-30",
"eventStartTime": "18:30",
"subtotalCents": 9000,
"discountCents": 0,
"feeCents": 350,
"taxCents": 630,
"totalCents": 9980,
"refundedCents": 0,
"total": "99.80",
"refunded": "0.00",
"promoCode": null,
"marketingOptIn": true,
"paidAt": "2026-10-04T20:31:09.000Z",
"createdAt": "2026-10-04T20:29:47.000Z",
"updatedAt": "2026-10-04T20:31:09.000Z"
}
Waitlist Entry
A party on the waitlist.
id | integer Id. Stable. |
|---|
guestName | string, or null Name. |
|---|
email | string, or null Email, if given. |
|---|
phone | string, or null Phone, if given. |
|---|
partySize | integer, or null Number of guests. |
|---|
date | string, or null Venue-local date. |
|---|
requestedTime | string, or null A time they asked for, HH:MM, if any. |
|---|
windowStart | string, or null Start of the window they can come in, HH:MM, if any. |
|---|
windowEnd | string, or null End of that window, HH:MM, if any. |
|---|
status | string, or null waiting, offered, booked (seated or given a table), expired or cancelled. |
|---|
source | string, or null Who added them: public (the online list) or staff. |
|---|
notes | string, or null The party's own request. |
|---|
quotedMinutes | integer, or null The latest wait quoted, in minutes. |
|---|
quotedAt | string, or null When that quote was given. |
|---|
pagedAt | string, or null When they were told their table is ready. |
|---|
seatedAt | string, or null When they were seated. |
|---|
leftAt | string, or null When they left without being seated. |
|---|
createdAt | string, or null When they joined the list. |
|---|
updatedAt | string, or null When the entry last changed. |
|---|
Example
{
"id": 902,
"guestName": "Sam Okafor",
"email": null,
"phone": "(555) 010-9310",
"partySize": 2,
"date": "2026-10-04",
"requestedTime": null,
"windowStart": null,
"windowEnd": null,
"status": "waiting",
"source": "staff",
"notes": "High chair",
"quotedMinutes": 25,
"quotedAt": "2026-10-04T23:02:00.000Z",
"pagedAt": null,
"seatedAt": null,
"leftAt": null,
"createdAt": "2026-10-04T23:02:00.000Z",
"updatedAt": "2026-10-04T23:02:00.000Z"
}
Lead
A private-event enquiry on the venue's Pipeline, with its contact. Internal notes on the conversation, drafts and triage stay in Parlor.
id | integer Id. Stable. |
|---|
contactName | string, or null Who the enquiry is from. |
|---|
contactEmail | string, or null Their email. |
|---|
contactPhone | string, or null Their phone. |
|---|
eventName | string, or null What the event is called. |
|---|
occasion | string, or null Occasion. |
|---|
date | string, or null The event date as given (free text; usually YYYY-MM-DD). |
|---|
guestCount | string, or null Guest count as given. |
|---|
budgetPerPerson | string, or null Budget per person as given. |
|---|
diningStyle | string, or null Dining style. |
|---|
dietaryNeeds | string, or null Dietary needs as given by the contact. |
|---|
stage | string, or null Pipeline stage: Inquiry, Proposal, Booked, ... |
|---|
priority | string, or null High, Medium or Low. |
|---|
notes | string, or null Notes. |
|---|
submittedVia | string, or null How it arrived. |
|---|
submittedAt | string, or null When it was submitted. |
|---|
createdAt | string, or null When it was made in Parlor. |
|---|
updatedAt | string, or null The latest change Parlor has a time for. |
|---|
Example
{
"id": 1790953960118,
"contactName": "Jordan Ellis",
"contactEmail": "[email protected]",
"contactPhone": "(555) 010-4477",
"eventName": "Rehearsal dinner",
"occasion": "Wedding",
"date": "2026-12-12",
"guestCount": "40",
"budgetPerPerson": "75",
"diningStyle": "Plated",
"dietaryNeeds": "Two vegetarian",
"stage": "Inquiry",
"priority": "Medium",
"notes": "Would like the garden room",
"submittedVia": "zapier",
"submittedAt": "2026-10-02T15:12:40.118Z",
"createdAt": "2026-10-02T15:12:40.118Z",
"updatedAt": "2026-10-02T15:12:40.118Z"
}
Task
A line on the venue's Tasks tab.
id | integer Id. |
|---|
title | string, or null What to do. |
|---|
due | string, or null Due date, YYYY-MM-DD, or null. |
|---|
priority | string: High, Medium, Low, or null Priority. |
|---|
category | string, or null Menu, Finance, Operations, POS, HR, Follow-Up or Other. |
|---|
client | string, or null Who it is about, if anyone. |
|---|
done | boolean Always false for a new task. |
|---|
Example
{
"id": 1791032400000,
"title": "Call Jordan about the rehearsal dinner",
"due": "2026-10-05",
"priority": "High",
"category": "Follow-Up",
"client": "Jordan Ellis",
"done": false
}
Endpoints
Every endpoint, with real request and response bodies. The same thing, machine-readable, is openapi.json (OpenAPI 3.1): load it into Postman, Insomnia or a code generator.
Account
Checking a key.
GET /api/v1/me
Check a key
Who this key belongs to, and what kind of key it is. Use it to test a connection.
Any key (Read-only is enough)
Answers
200 A working key
{
"venue": {
"slug": "cotillion",
"name": "Cotillion Cafe",
"timezone": "America/New_York"
},
"plan": "pro",
"key": {
"kind": "api",
"scope": "read"
}
}
404 The key's venue could not be found.
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Reservations
GET /api/v1/reservations
List reservations
Newest first, by when each was created. Up to 100 per call (default 25). In a list, a reservation's updatedAt reflects arrival, seating and cancellation; edits to its time or size arrive through webhooks.
Any key (Read-only is enough)
Parameters
limit query | How many to return, 1 to 100 (default 25). Larger numbers are treated as 100. |
|---|
since query | Only records created at or after this moment. Include a time zone, e.g. 2026-10-01T00:00:00-04:00. |
|---|
Answers
200 Newest reservations
[
{
"id": 48213,
"guestName": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"partySize": 4,
"date": "2026-10-17",
"time": "19:30",
"durationMinutes": 90,
"status": "Confirmed",
"source": "public",
"bookingType": "Dinner",
"notes": "Anniversary -- window table if possible",
"floorStage": null,
"createdAt": "2026-10-02T14:05:11.482Z",
"checkedInAt": null,
"cancelledAt": null,
"cancelledBy": null,
"updatedAt": "2026-10-02T14:05:11.482Z"
}
]
400 limit or since could not be read.
since unreadable
{
"error": "since must be a date and time, for example 2026-09-29T18:00:00-04:00."
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
POST /api/v1/reservations
Request a reservation
Always made as a REQUEST (status Requested, no table) for staff to confirm. It is checked first against the venue's hours, closed days, blocks, same-day cut-off, party limits, notice, how far ahead, last arrival, set seating times and pacing, and refused with a reason if any of them says no. No confirmation email is sent to the guest, because nothing is confirmed yet.
Read and write key
Parameters
Idempotency-Key header | Any unique string you choose (1 to 200 visible characters). A retry with the same key and the same body within 24 hours gets the first answer back instead of doing it twice. |
|---|
Body
guestName required | string Name for the booking. |
|---|
email | string Guest email. |
|---|
phone | string Guest phone. |
|---|
partySize required | integer Number of guests. |
|---|
date | string Venue-local date, YYYY-MM-DD. |
|---|
time | string Venue-local time: 19:00, 7:00 PM or 7pm. |
|---|
startsAt | string Instead of date and time: an ISO date-time WITH an offset. Converted to the venue's clock. |
|---|
notes | string The guest's request. |
|---|
Example request body
{
"guestName": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"partySize": 4,
"date": "2026-10-17",
"time": "7:30 PM",
"notes": "From the website enquiry form"
}
Answers
201 A reservation request
{
"reservation": {
"id": 48260,
"guestName": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"partySize": 4,
"date": "2026-10-17",
"time": "19:30",
"durationMinutes": 90,
"status": "Requested",
"source": "zapier",
"bookingType": null,
"notes": "From the website enquiry form",
"floorStage": null,
"createdAt": "2026-10-03T16:40:02.114Z",
"checkedInAt": null,
"cancelledAt": null,
"cancelledBy": null,
"updatedAt": "2026-10-03T16:40:02.114Z"
}
}
400 Something in the body is missing or unreadable.
No name
{
"error": "guestName is required."
}
409 The venue can't take that booking (reason says why), or an Idempotency-Key clash (see Safe retries).
Closed that day
{
"error": "The venue is closed that day (Closed for Thanksgiving).",
"reason": "closed"
}
Fully booked
{
"error": "The venue is fully booked at that time -- it has reached the number of guests it takes in that slot.",
"reason": "pacing-covers-full"
}
In the past
{
"error": "That time has already passed at the venue (it is 2026-10-17 20:05 there now).",
"reason": "in-the-past"
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
GET /api/v1/reservations/find
Find a guest's reservations
By the guest's email, or a phone number with at least 7 digits. Upcoming first (soonest first), then past (most recent first). At most 10. An empty array means none found.
Any key (Read-only is enough)
Parameters
email query | Guest email. |
|---|
phone query | Guest phone, at least 7 digits. |
|---|
date query | Only this venue-local date. |
|---|
Answers
200 One upcoming booking
[
{
"id": 48213,
"guestName": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"partySize": 4,
"date": "2026-10-17",
"time": "19:30",
"durationMinutes": 90,
"status": "Confirmed",
"source": "public",
"bookingType": "Dinner",
"notes": "Anniversary -- window table if possible",
"floorStage": null,
"createdAt": "2026-10-02T14:05:11.482Z",
"checkedInAt": null,
"cancelledAt": null,
"cancelledBy": null,
"updatedAt": "2026-10-02T14:05:11.482Z"
}
]
400 Neither an email nor a usable phone number was given.
Nothing to search by
{
"error": "Search by the guest's email, or a phone number with at least 7 digits."
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Guests
GET /api/v1/guests
List guests
Newest first, by when each was created. Up to 100 per call (default 25).
Any key (Read-only is enough)
Parameters
limit query | How many to return, 1 to 100 (default 25). Larger numbers are treated as 100. |
|---|
since query | Only records created at or after this moment. Include a time zone, e.g. 2026-10-01T00:00:00-04:00. |
|---|
Answers
200 Newest guests
[
{
"id": 1760,
"name": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"seatingPreference": "Booth",
"referralSource": "Website",
"visitCount": 6,
"noShowCount": 0,
"cancellationCount": 1,
"lastVisitDate": "2026-09-12",
"createdAt": "2025-11-03T17:22:40.000Z",
"updatedAt": "2026-09-12T23:10:02.000Z"
}
]
400 limit or since could not be read.
since unreadable
{
"error": "since must be a date and time, for example 2026-09-29T18:00:00-04:00."
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
POST /api/v1/guests
Create or update a guest
Matched the way Parlor matches everywhere: exact email first, then phone. A matched guest's name, email and phone are only FILLED IN where empty, never replaced; a different value is written into the guest's notes for staff and returned in conflicting. Notes are added to, never replaced.
Read and write key
Parameters
Idempotency-Key header | Any unique string you choose (1 to 200 visible characters). A retry with the same key and the same body within 24 hours gets the first answer back instead of doing it twice. |
|---|
Body
name | string Full name. |
|---|
email | string Email address. |
|---|
phone | string Phone number. |
|---|
notes | string Added to the guest's notes. Never replaces what staff wrote. |
|---|
Example request body
{
"name": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477"
}
Answers
200 Matched, with a different phone kept aside
{
"created": false,
"guest": {
"id": 1760,
"name": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"seatingPreference": "Booth",
"referralSource": "Website",
"visitCount": 6,
"noShowCount": 0,
"cancellationCount": 1,
"lastVisitDate": "2026-09-12",
"createdAt": "2025-11-03T17:22:40.000Z",
"updatedAt": "2026-09-12T23:10:02.000Z"
},
"conflicting": {
"phone": "(555) 010-9999"
}
}
201 New guest
{
"created": true,
"guest": {
"id": 1760,
"name": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"seatingPreference": "Booth",
"referralSource": "Website",
"visitCount": 6,
"noShowCount": 0,
"cancellationCount": 1,
"lastVisitDate": "2026-09-12",
"createdAt": "2025-11-03T17:22:40.000Z",
"updatedAt": "2026-09-12T23:10:02.000Z"
}
}
400 No email and no usable phone.
No way to tell guests apart
{
"error": "A guest needs an email address or a phone number (at least 7 digits), so Parlor can tell them apart."
}
409 An Idempotency-Key clash: see Safe retries.
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
GET /api/v1/guests/find
Find a guest
By email, or a phone number with at least 7 digits. Answers an array with at most one guest; empty means not found.
Any key (Read-only is enough)
Parameters
email query | Email address. |
|---|
phone query | Phone, at least 7 digits. |
|---|
Answers
200 Found
[
{
"id": 1760,
"name": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"seatingPreference": "Booth",
"referralSource": "Website",
"visitCount": 6,
"noShowCount": 0,
"cancellationCount": 1,
"lastVisitDate": "2026-09-12",
"createdAt": "2025-11-03T17:22:40.000Z",
"updatedAt": "2026-09-12T23:10:02.000Z"
}
]
400 Nothing to search by.
Nothing to search by
{
"error": "Search by email, or by a phone number with at least 7 digits."
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Subscribers
GET /api/v1/subscribers
List subscribers
Newest first, by when each was created. Up to 100 per call (default 25).
Any key (Read-only is enough)
Parameters
limit query | How many to return, 1 to 100 (default 25). Larger numbers are treated as 100. |
|---|
since query | Only records created at or after this moment. Include a time zone, e.g. 2026-10-01T00:00:00-04:00. |
|---|
Answers
200 Newest subscribers
[
{
"id": 8812,
"email": "[email protected]",
"firstName": "Jordan",
"status": "subscribed",
"source": "signup_form",
"birthMonth": 4,
"birthDay": 22,
"consentAt": "2026-03-14T18:02:55.000Z",
"confirmedAt": "2026-03-14T18:04:10.000Z",
"unsubscribedAt": null,
"createdAt": "2026-03-14T18:02:55.000Z",
"updatedAt": "2026-03-14T18:04:10.000Z"
}
]
400 limit or since could not be read.
since unreadable
{
"error": "since must be a date and time, for example 2026-09-29T18:00:00-04:00."
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
POST /api/v1/subscribers
Add an email subscriber
Needs proof of consent. Someone who unsubscribed is never added back, an address on the do-not-email list is never added, and someone waiting to confirm stays waiting. Those outcomes are a 200 with added: false and a sentence, not an error.
Read and write key
Parameters
Idempotency-Key header | Any unique string you choose (1 to 200 visible characters). A retry with the same key and the same body within 24 hours gets the first answer back instead of doing it twice. |
|---|
Body
email required | string Email address. |
|---|
firstName | string First name. |
|---|
tag | string A tag to file them under. |
|---|
consent required | object Proof this person agreed to marketing email. Required. Stored as the venue's evidence. source string Where they agreed, e.g. "Website contact form".
statement string The exact words they agreed to.
at string When they agreed, with a time zone. Not in the future.
pageUrl string The page they agreed on, if any.
|
|---|
Example request body
{
"email": "[email protected]",
"firstName": "Jordan",
"consent": {
"source": "Website contact form",
"statement": "Yes, send me news and offers from Cotillion Cafe by email.",
"at": "2026-10-03T12:14:00-04:00",
"pageUrl": "https://example.com/contact"
}
}
Answers
200 They unsubscribed earlier
{
"added": false,
"status": "unsubscribed",
"message": "Not added: this person unsubscribed from the venue's emails. Parlor never adds them back on someone else's say-so -- they can rejoin themselves through the venue's signup form.",
"subscriber": {
"id": 8812,
"email": "[email protected]",
"firstName": "Jordan",
"status": "unsubscribed",
"source": "signup_form",
"birthMonth": 4,
"birthDay": 22,
"consentAt": "2026-03-14T18:02:55.000Z",
"confirmedAt": "2026-03-14T18:04:10.000Z",
"unsubscribedAt": "2026-06-01T10:00:00.000Z",
"createdAt": "2026-03-14T18:02:55.000Z",
"updatedAt": "2026-06-01T10:00:00.000Z"
}
}
200 On the do-not-email list
{
"added": false,
"status": "suppressed",
"message": "Not added: this address is on the venue's do-not-email list (it bounced, was reported as spam, or was removed by staff).",
"subscriber": null
}
201 Added
{
"added": true,
"status": "subscribed",
"message": "Added to the venue's email list.",
"subscriber": {
"id": 8812,
"email": "[email protected]",
"firstName": "Jordan",
"status": "subscribed",
"source": "signup_form",
"birthMonth": 4,
"birthDay": 22,
"consentAt": "2026-03-14T18:02:55.000Z",
"confirmedAt": "2026-03-14T18:04:10.000Z",
"unsubscribedAt": null,
"createdAt": "2026-03-14T18:02:55.000Z",
"updatedAt": "2026-03-14T18:04:10.000Z"
}
}
400 The address or the consent evidence is missing or wrong.
No consent
{
"error": "consent.source is missing -- say where they agreed, e.g. \"Website contact form\"."
}
409 An Idempotency-Key clash: see Safe retries.
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Ticket orders
GET /api/v1/ticket-orders
List ticket orders
Newest first, by when each was created. Up to 100 per call (default 25). Abandoned and in-flight checkouts are left out.
Any key (Read-only is enough)
Parameters
limit query | How many to return, 1 to 100 (default 25). Larger numbers are treated as 100. |
|---|
since query | Only records created at or after this moment. Include a time zone, e.g. 2026-10-01T00:00:00-04:00. |
|---|
Answers
200 Newest ticket orders
[
{
"id": 311,
"orderNumber": "MR-7K4Q2",
"status": "paid",
"channel": "public",
"paymentMethod": "square_link",
"buyerFirstName": "Jordan",
"buyerLastName": "Ellis",
"buyerEmail": "[email protected]",
"buyerPhone": "(555) 010-4477",
"eventName": "Murder Mystery Supper",
"occurrenceTitle": "Friday seating",
"eventDate": "2026-10-30",
"eventStartTime": "18:30",
"subtotalCents": 9000,
"discountCents": 0,
"feeCents": 350,
"taxCents": 630,
"totalCents": 9980,
"refundedCents": 0,
"total": "99.80",
"refunded": "0.00",
"promoCode": null,
"marketingOptIn": true,
"paidAt": "2026-10-04T20:31:09.000Z",
"createdAt": "2026-10-04T20:29:47.000Z",
"updatedAt": "2026-10-04T20:31:09.000Z"
}
]
400 limit or since could not be read.
since unreadable
{
"error": "since must be a date and time, for example 2026-09-29T18:00:00-04:00."
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Waitlist
GET /api/v1/waitlist
List waitlist entries
Newest first, by when each was created. Up to 100 per call (default 25).
Any key (Read-only is enough)
Parameters
limit query | How many to return, 1 to 100 (default 25). Larger numbers are treated as 100. |
|---|
since query | Only records created at or after this moment. Include a time zone, e.g. 2026-10-01T00:00:00-04:00. |
|---|
Answers
200 Newest waitlist entries
[
{
"id": 902,
"guestName": "Sam Okafor",
"email": null,
"phone": "(555) 010-9310",
"partySize": 2,
"date": "2026-10-04",
"requestedTime": null,
"windowStart": null,
"windowEnd": null,
"status": "waiting",
"source": "staff",
"notes": "High chair",
"quotedMinutes": 25,
"quotedAt": "2026-10-04T23:02:00.000Z",
"pagedAt": null,
"seatedAt": null,
"leftAt": null,
"createdAt": "2026-10-04T23:02:00.000Z",
"updatedAt": "2026-10-04T23:02:00.000Z"
}
]
400 limit or since could not be read.
since unreadable
{
"error": "since must be a date and time, for example 2026-09-29T18:00:00-04:00."
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Leads
GET /api/v1/leads
List private-event leads
Newest first, by when each was created. Up to 100 per call (default 25).
Any key (Read-only is enough)
Parameters
limit query | How many to return, 1 to 100 (default 25). Larger numbers are treated as 100. |
|---|
since query | Only records created at or after this moment. Include a time zone, e.g. 2026-10-01T00:00:00-04:00. |
|---|
Answers
200 Newest private-event leads
[
{
"id": 1790953960118,
"contactName": "Jordan Ellis",
"contactEmail": "[email protected]",
"contactPhone": "(555) 010-4477",
"eventName": "Rehearsal dinner",
"occasion": "Wedding",
"date": "2026-12-12",
"guestCount": "40",
"budgetPerPerson": "75",
"diningStyle": "Plated",
"dietaryNeeds": "Two vegetarian",
"stage": "Inquiry",
"priority": "Medium",
"notes": "Would like the garden room",
"submittedVia": "zapier",
"submittedAt": "2026-10-02T15:12:40.118Z",
"createdAt": "2026-10-02T15:12:40.118Z",
"updatedAt": "2026-10-02T15:12:40.118Z"
}
]
400 limit or since could not be read.
since unreadable
{
"error": "since must be a date and time, for example 2026-09-29T18:00:00-04:00."
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
POST /api/v1/leads
Add a private-event enquiry
Lands on the venue's Pipeline at stage Inquiry. The contact is matched to an existing guest first, the same way POST /api/v1/guests matches.
Read and write key
Parameters
Idempotency-Key header | Any unique string you choose (1 to 200 visible characters). A retry with the same key and the same body within 24 hours gets the first answer back instead of doing it twice. |
|---|
Body
name required | string Who the enquiry is from. |
|---|
email | string Their email. |
|---|
phone | string Their phone. |
|---|
eventName | string What the event is called. Default "TBD". |
|---|
occasion | string Occasion. |
|---|
eventDate | string The date, YYYY-MM-DD. |
|---|
guestCount | string How many guests. |
|---|
budgetPerPerson | string Budget per person. |
|---|
concept | string Which room or brand. Defaults to the one the venue used last. |
|---|
priority | string: High, Medium, Low |
|---|
notes | string Anything else. |
|---|
Example request body
{
"name": "Jordan Ellis",
"email": "[email protected]",
"eventName": "Rehearsal dinner",
"occasion": "Wedding",
"eventDate": "2026-12-12",
"guestCount": "40",
"notes": "Would like the garden room"
}
Answers
201 New enquiry
{
"lead": {
"id": 1790953960118,
"contactName": "Jordan Ellis",
"contactEmail": "[email protected]",
"contactPhone": "(555) 010-4477",
"eventName": "Rehearsal dinner",
"occasion": "Wedding",
"date": "2026-12-12",
"guestCount": "40",
"budgetPerPerson": "75",
"diningStyle": "Plated",
"dietaryNeeds": "Two vegetarian",
"stage": "Inquiry",
"priority": "Medium",
"notes": "Would like the garden room",
"submittedVia": "zapier",
"submittedAt": "2026-10-02T15:12:40.118Z",
"createdAt": "2026-10-02T15:12:40.118Z",
"updatedAt": "2026-10-02T15:12:40.118Z"
}
}
400 Something is missing.
No name
{
"error": "name is required -- who is the inquiry from?"
}
409 An Idempotency-Key clash, or an earlier try never finished (reason outcome-unknown): see Safe retries.
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Tasks
POST /api/v1/tasks
Add a task
A line on the venue's Tasks tab. Not protected against repeats by default: send an Idempotency-Key if a retry must not add it twice.
Read and write key
Parameters
Idempotency-Key header | Any unique string you choose (1 to 200 visible characters). A retry with the same key and the same body within 24 hours gets the first answer back instead of doing it twice. |
|---|
Body
title required | string What to do. |
|---|
due | string Due date, YYYY-MM-DD. |
|---|
priority | string: High, Medium, Low Default Medium. |
|---|
category | string: Menu, Finance, Operations, POS, HR, Follow-Up, Other Anything else is filed as Other. |
|---|
client | string Who it is about. |
|---|
Example request body
{
"title": "Call Jordan about the rehearsal dinner",
"due": "2026-10-05",
"priority": "High",
"category": "Follow-Up",
"client": "Jordan Ellis"
}
Answers
201 New task
{
"task": {
"id": 1791032400000,
"title": "Call Jordan about the rehearsal dinner",
"due": "2026-10-05",
"priority": "High",
"category": "Follow-Up",
"client": "Jordan Ellis",
"done": false
}
}
400 No title, or an unreadable date.
No title
{
"error": "title is required."
}
409 An Idempotency-Key clash, or an earlier try never finished (reason outcome-unknown): see Safe retries.
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Campaigns
Write a marketing email, build its list, test it and send it -- through the same checks as the dashboard. API keys only; a Zapier key is refused.
POST /api/v1/campaigns
Write a campaign
Saves a marketing email as a draft. Nothing is sent and nothing is scheduled: build the list with compose, send a test, then send.
Read and write API key (not a Zapier key)
Body
name required | string The internal name staff see in Parlor. |
|---|
subject required | string |
|---|
preheader | string The preview line after the subject. |
|---|
bodyHtml | string The email. Gmail clips anything over about 102 KB. |
|---|
bodyText | string The plain-text part. |
|---|
bodyFormat | string: guided, html html for a complete document of your own; guided (the default) to wrap it in the venue's design. |
|---|
segment | object Who it goes to. {"type": "all"} is everyone subscribed to the venue. |
|---|
Example request body
{
"name": "Lesson -- 2026-10-11 -- Why your steak never gets a crust",
"subject": "Why your steak never gets a crust",
"preheader": "Three small things before it ever hits the pan.",
"bodyHtml": "<!doctype html><html><body>...</body></html>",
"bodyText": "Why your steak never gets a crust ...",
"bodyFormat": "html",
"segment": {
"type": "all"
}
}
Answers
200 A draft
{
"ok": true,
"campaign": {
"id": 42,
"restaurantId": "3f6c2a1e-9b7d-4c55-a1f0-2d8e6b4c9a17",
"name": "Lesson -- 2026-10-11 -- Why your steak never gets a crust",
"subject": "Why your steak never gets a crust",
"preheader": "Three small things before it ever hits the pan.",
"bodyHtml": "<!doctype html><html><body>...</body></html>",
"bodyText": "Why your steak never gets a crust ...",
"bodyFormat": "html",
"segment": {
"type": "all"
},
"status": "draft",
"scheduledAt": null,
"startedAt": null,
"finishedAt": null,
"testSentAt": null,
"createdBy": null,
"recipientsTotal": 0,
"sentCount": 0,
"failedCount": 0,
"lastError": "",
"sendHoldUntil": null,
"eventId": null,
"seriesKey": "",
"seriesStep": null,
"seriesTotal": null,
"createdAt": "2026-10-11T12:02:11.000Z",
"updatedAt": "2026-10-11T12:02:11.000Z"
}
}
400 Something in the body is missing or wrong.
Subject too long
{
"error": "A subject line over 200 characters is truncated by most mail clients — shorten it."
}
403 As for any 403, and also: a read-only key, or a Zapier key. Email campaigns need an API key.
A read-only key
{
"error": "This API key is read-only, so it can look things up but can't add, change or remove anything. Create a Read and write key in Parlor under Settings -> Integrations & API if this app needs to make changes."
}
A Zapier key
{
"error": "Email campaigns need a Parlor API key, not a Zapier key. Request API access under Settings -> Integrations & API, then create an API key there.",
"reason": "api-key-required"
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
POST /api/v1/campaigns/compose
Build the list
Works out who the campaign goes to and queues them. Safe to repeat: someone already queued is not queued twice. It never schedules the campaign, even one that already has a send time -- that is send's job alone.
Read and write API key (not a Zapier key)
Body
id required | integer |
|---|
segment | object Change who it goes to while building the list. Leave it out to use the campaign's own. |
|---|
Example request body
{
"id": 42
}
Answers
200 Built
{
"ok": true,
"campaign": {
"id": 42,
"restaurantId": "3f6c2a1e-9b7d-4c55-a1f0-2d8e6b4c9a17",
"name": "Lesson -- 2026-10-11 -- Why your steak never gets a crust",
"subject": "Why your steak never gets a crust",
"preheader": "Three small things before it ever hits the pan.",
"bodyHtml": "<!doctype html><html><body>...</body></html>",
"bodyText": "Why your steak never gets a crust ...",
"bodyFormat": "html",
"segment": {
"type": "all"
},
"status": "draft",
"scheduledAt": null,
"startedAt": null,
"finishedAt": null,
"testSentAt": null,
"createdBy": null,
"recipientsTotal": 412,
"sentCount": 0,
"failedCount": 0,
"lastError": "",
"sendHoldUntil": null,
"eventId": null,
"seriesKey": "",
"seriesStep": null,
"seriesTotal": null,
"createdAt": "2026-10-11T12:02:11.000Z",
"updatedAt": "2026-10-11T12:02:40.000Z"
},
"recipientsTotal": 412,
"heldBack": 0,
"holdoutPct": 0,
"holdUntil": null,
"added": 412,
"alreadyPresent": 0,
"removed": 0,
"keptInFlight": 0,
"matched": 431,
"matchedKnown": true,
"noEmail": 0,
"unsubscribed": 12,
"suppressed": 7,
"excludedTicketHolders": 0,
"reliable": true,
"notes": []
}
404 No campaign with that id at this venue.
Not found
{
"error": "No such campaign."
}
403 As for any 403, and also: a read-only key, or a Zapier key. Email campaigns need an API key.
A read-only key
{
"error": "This API key is read-only, so it can look things up but can't add, change or remove anything. Create a Read and write key in Parlor under Settings -> Integrations & API if this app needs to make changes."
}
A Zapier key
{
"error": "Email campaigns need a Parlor API key, not a Zapier key. Request API access under Settings -> Integrations & API, then create an API key there.",
"reason": "api-key-required"
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
POST /api/v1/campaigns/test
Send a test
Sends one copy to each of the venue's saved test addresses (Settings -> Marketing email). The address is never taken from the request -- that would let a key mail anyone in the venue's name. A campaign cannot be sent until a test has reached the first of them.
Read and write API key (not a Zapier key)
Body
id required | integer The campaign's id. |
|---|
Example request body
{
"id": 42
}
Answers
200 Sent
{
"ok": true,
"to": "[email protected]",
"testSentAt": "2026-10-11T12:03:05.000Z",
"messageId": "0100018f-3c2a-7d1e",
"alsoSent": [
"[email protected]"
],
"alsoFailed": []
}
409 There is no test address saved for this venue.
No test address saved
{
"error": "There is no test address saved for this venue, so there is nowhere to send a test. Add one under Settings -> Marketing email -> Test recipients.",
"reason": "no-test-address"
}
403 As for any 403, and also: a read-only key, or a Zapier key. Email campaigns need an API key.
A read-only key
{
"error": "This API key is read-only, so it can look things up but can't add, change or remove anything. Create a Read and write key in Parlor under Settings -> Integrations & API if this app needs to make changes."
}
A Zapier key
{
"error": "Email campaigns need a Parlor API key, not a Zapier key. Request API access under Settings -> Integrations & API, then create an API key there.",
"reason": "api-key-required"
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
POST /api/v1/campaigns/send
Send or schedule it
Puts the campaign on its schedule through every check the dashboard's Send button makes: a test must have reached the venue, the list must be built, the plan must allow it, and the pre-send checklist must have no blockers.
Read and write API key (not a Zapier key)
Body
id required | integer |
|---|
scheduledAt | string When to send, with a time zone. Leave it out to send now: a send-now waits ten minutes first, and POST /api/v1/campaigns/cancel stops it in that time with nobody having received it. |
|---|
confirmCount | integer How many people you expect it to go to (recipientsTotal from compose). If the list has changed since, nothing is sent and you get 409 count-changed. |
|---|
Example request body
{
"id": 42,
"scheduledAt": "2026-10-11T09:00:00-04:00",
"confirmCount": 412
}
Answers
200 Scheduled for 9am Eastern
{
"ok": true,
"campaign": {
"id": 42,
"restaurantId": "3f6c2a1e-9b7d-4c55-a1f0-2d8e6b4c9a17",
"name": "Lesson -- 2026-10-11 -- Why your steak never gets a crust",
"subject": "Why your steak never gets a crust",
"preheader": "Three small things before it ever hits the pan.",
"bodyHtml": "<!doctype html><html><body>...</body></html>",
"bodyText": "Why your steak never gets a crust ...",
"bodyFormat": "html",
"segment": {
"type": "all"
},
"status": "scheduled",
"scheduledAt": "2026-10-11T13:00:00.000Z",
"startedAt": "2026-10-11T12:03:30.000Z",
"finishedAt": null,
"testSentAt": "2026-10-11T12:03:05.000Z",
"createdBy": null,
"recipientsTotal": 412,
"sentCount": 0,
"failedCount": 0,
"lastError": "",
"sendHoldUntil": null,
"eventId": null,
"seriesKey": "",
"seriesStep": null,
"seriesTotal": null,
"createdAt": "2026-10-11T12:02:11.000Z",
"updatedAt": "2026-10-11T12:02:11.000Z"
},
"recipientsTotal": 412,
"pending": 412,
"retried": 0,
"holdUntil": null
}
402 Over the venue's plan allowance. Nothing is sent; the campaign stays a draft.
Over the plan
{
"error": "This send would go over the emails included in this month's plan. See Settings -> Billing to add more.",
"reason": "plan-limit"
}
409 Not sent, and why. The campaign is left exactly as it was.
No test yet
{
"error": "Send yourself a test first — a campaign nobody has seen rendered must not go to your whole list.",
"reason": "no-test-send"
}
No list yet
{
"error": "Compose the audience first — this campaign has nobody in it.",
"reason": "no-recipients"
}
The list changed
{
"error": "This list has changed since you looked at it: it now has 415 people, not 412. Check it and try again.",
"reason": "count-changed"
}
403 As for any 403, and also: a read-only key, or a Zapier key. Email campaigns need an API key.
A read-only key
{
"error": "This API key is read-only, so it can look things up but can't add, change or remove anything. Create a Read and write key in Parlor under Settings -> Integrations & API if this app needs to make changes."
}
A Zapier key
{
"error": "Email campaigns need a Parlor API key, not a Zapier key. Request API access under Settings -> Integrations & API, then create an API key there.",
"reason": "api-key-required"
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
POST /api/v1/campaigns/cancel
Cancel it
Stops a scheduled or sending campaign. Anyone not yet sent to is skipped, not marked failed. Copies already delivered cannot be called back.
Read and write API key (not a Zapier key)
Body
id required | integer The campaign's id. |
|---|
Example request body
{
"id": 42
}
Answers
200 Cancelled before it started
{
"ok": true,
"campaign": {
"id": 42,
"restaurantId": "3f6c2a1e-9b7d-4c55-a1f0-2d8e6b4c9a17",
"name": "Lesson -- 2026-10-11 -- Why your steak never gets a crust",
"subject": "Why your steak never gets a crust",
"preheader": "Three small things before it ever hits the pan.",
"bodyHtml": "<!doctype html><html><body>...</body></html>",
"bodyText": "Why your steak never gets a crust ...",
"bodyFormat": "html",
"segment": {
"type": "all"
},
"status": "cancelled",
"scheduledAt": "2026-10-11T13:00:00.000Z",
"startedAt": null,
"finishedAt": "2026-10-11T12:40:00.000Z",
"testSentAt": "2026-10-11T12:03:05.000Z",
"createdBy": null,
"recipientsTotal": 412,
"sentCount": 0,
"failedCount": 0,
"lastError": "",
"sendHoldUntil": null,
"eventId": null,
"seriesKey": "",
"seriesStep": null,
"seriesTotal": null,
"createdAt": "2026-10-11T12:02:11.000Z",
"updatedAt": "2026-10-11T12:02:11.000Z"
},
"counts": {
"total": 412,
"pending": 0,
"sent": 0,
"failed": 0,
"skipped": 412,
"heldBack": 0
}
}
409 Already sent or already cancelled.
Already sent
{
"error": "This campaign is already sent."
}
403 As for any 403, and also: a read-only key, or a Zapier key. Email campaigns need an API key.
A read-only key
{
"error": "This API key is read-only, so it can look things up but can't add, change or remove anything. Create a Read and write key in Parlor under Settings -> Integrations & API if this app needs to make changes."
}
A Zapier key
{
"error": "Email campaigns need a Parlor API key, not a Zapier key. Request API access under Settings -> Integrations & API, then create an API key there.",
"reason": "api-key-required"
}
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Webhooks
Your own endpoints, for API keys with Read and write access.
GET /api/v1/webhooks
List your webhook endpoints
Newest first, with how the last delivery went. Needs an API key with Read and write access. Secrets are never shown again.
API key with Read and write access
Answers
200 One endpoint
[
{
"id": "3f1c9a52-8d0e-4b7a-9c61-2e5d7a0b4f18",
"url": "https://hooks.example.com/parlor",
"events": [
"reservation.cancelled",
"reservation.created"
],
"status": "active",
"disabledReason": null,
"failureCount": 0,
"lastDeliveryAt": "2026-10-04T20:31:10.000Z",
"lastDeliveryStatus": "ok 200",
"createdAt": "2026-10-01T15:00:00.000Z"
}
]
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
POST /api/v1/webhooks
Add a webhook endpoint
Parlor will POST each chosen event to url, signed. The secret is in this reply and NEVER again. https only, standard port, a public domain name. Redirects are not followed. At most 10 endpoints per venue, and one per address: to change an endpoint's events, delete it and add it again. An endpoint belongs to the key that made it, and stops if that key is revoked.
API key with Read and write access
Body
url required | string Where to send deliveries. https, standard port, a public domain name: no IP addresses, no names that only work inside one network, no user:password@. |
|---|
events required | array of EventName Which events to send. |
|---|
Example request body
{
"url": "https://hooks.example.com/parlor",
"events": [
"reservation.cancelled",
"reservation.created"
]
}
Answers
201 New endpoint
{
"id": "3f1c9a52-8d0e-4b7a-9c61-2e5d7a0b4f18",
"url": "https://hooks.example.com/parlor",
"events": [
"reservation.cancelled",
"reservation.created"
],
"status": "active",
"createdAt": "2026-10-01T15:00:00.000Z",
"secret": "whsec_4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e"
}
400 The address or the events are not acceptable.
An IP address
{
"error": "Parlor can't send to that address: it is a raw IP address -- use your server's domain name, for example https://hooks.example.com/parlor."
}
409 The venue already has an endpoint for that address, or already has 10.
413 The body is over 16 KB.
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
DELETE /api/v1/webhooks/{id}
Remove a webhook endpoint
Stops deliveries to it at once.
API key with Read and write access
Parameters
id path required | The endpoint's id. |
|---|
Answers
200 Removed
{
"id": "3f1c9a52-8d0e-4b7a-9c61-2e5d7a0b4f18",
"deleted": true
}
404 No such endpoint for this venue.
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
POST /api/v1/webhooks/{id}/test
Send a test delivery
Sends one signed ping to the endpoint now and tells you what your server answered, so you can check your signature code. A test never counts towards the endpoint's failures.
API key with Read and write access
Parameters
id path required | The endpoint's id. |
|---|
Answers
200 Your server answered 200
{
"id": "0d9e8f7a-6b5c-4d3e-8f1a-2b3c4d5e6f70",
"event": "ping",
"ok": true,
"statusCode": 200,
"error": null,
"durationMs": 184
}
200 Your server answered 401
{
"id": "0d9e8f7a-6b5c-4d3e-8f1a-2b3c4d5e6f70",
"event": "ping",
"ok": false,
"statusCode": 401,
"error": "HTTP 401",
"durationMs": 97
}
404 No such endpoint for this venue.
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Zapier
Used by Parlor's Zapier app. Not for your own server.
POST /api/v1/hooks
Subscribe a Zap (Zapier only)
Used by Parlor's Zapier app when a Zap is switched on. Only accepts hooks.zapier.com addresses. For your own server, use /api/v1/webhooks.
Used by Parlor's Zapier app only
Body
event required | EventName Something that happened at the venue. |
|---|
targetUrl required | string |
|---|
Answers
201 A Zap
{
"id": "8b2e4f60-1c3d-4a5b-9e7f-0a1b2c3d4e5f",
"event": "reservation.created",
"targetUrl": "https://hooks.zapier.com/hooks/standard/123456/abcdef/"
}
400 Unknown event or a non-Zapier address.
409 Too many Zaps are listening.
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
DELETE /api/v1/hooks/{id}
Unsubscribe a Zap (Zapier only)
Used by Parlor's Zapier app only
Parameters
id path required | The hook's id. |
|---|
Answers
200 Removed
{
"id": "8b2e4f60-1c3d-4a5b-9e7f-0a1b2c3d4e5f",
"deleted": true
}
404 No such hook.
Any endpoint can also answer 401, 403, 429 or 503: see Errors.
Webhooks
A webhook sends an event to your server the moment it happens, so you do not have to keep asking. Add an endpoint with a Read and write key (see Add a webhook endpoint). The reply includes a secret, shown only that once: store it with your other secrets.
Events
| Event | When | data is |
reservation.created | A reservation is made. | Reservation |
reservation.updated | A reservation is changed (time, size, status, arrival, seating). | Reservation |
reservation.cancelled | A reservation is cancelled. | Reservation |
guest.created | A new guest record is made. | Guest |
subscriber.added | Someone joins the email list. | Subscriber |
subscriber.unsubscribed | Someone leaves the email list. | Subscriber |
ticket_order.paid | A ticket order is paid. | Ticket Order |
ticket_order.refunded | A ticket order is refunded, fully or partly. | Ticket Order |
waitlist.added | A party joins the waitlist. | Waitlist Entry |
waitlist.seated | A waitlist party is seated. | Waitlist Entry |
lead.created | A private-event enquiry arrives. | Lead |
What arrives
A POST with a JSON body and these headers:
X-Parlor-Event: the event name.
X-Parlor-Signature: t=<unix seconds>,v1=<signature>. See Verifying signatures.
User-Agent: Parlor-Webhooks/1
{
"id": "b7f3c2a4-5d6e-4f70-8a9b-0c1d2e3f4a5b",
"event": "reservation.created",
"occurredAt": "2026-10-02T14:05:11.482Z",
"venue": {
"slug": "cotillion"
},
"data": {
"id": 48213,
"guestName": "Jordan Ellis",
"email": "[email protected]",
"phone": "(555) 010-4477",
"partySize": 4,
"date": "2026-10-17",
"time": "19:30",
"durationMinutes": 90,
"status": "Confirmed",
"source": "public",
"bookingType": "Dinner",
"notes": "Anniversary -- window table if possible",
"floorStage": null,
"createdAt": "2026-10-02T14:05:11.482Z",
"checkedInAt": null,
"cancelledAt": null,
"cancelledBy": null,
"updatedAt": "2026-10-02T14:05:11.482Z"
}
}
Answering, retries and switch-off
- Answer with any 2xx within 10 seconds. Do the real work afterwards. Parlor does not follow redirects: a 3xx counts as a failure.
- A failed delivery is tried again after 1, 5, 15 and 60 minutes, then 3, 6, 12 and 24 hours: nine tries over about two days. An event still undelivered 48 hours after it happened is dropped.
- The body's
id is the same on every try. Keep the ids you have handled and ignore repeats: a delivery can arrive twice if your answer was lost.
- Order is not guaranteed. A retry can arrive after a later event. Use
occurredAt, and data.updatedAt, to tell which is newest.
- An endpoint that fails 20 times in a row is switched off, and the owner sees it in Settings. Answering
410 Gone switches it off at once.
- Each venue's events go out at up to 60 a minute. A busy import sends in batches.
Where Parlor will send
- An
https:// address on the standard port, with a public domain name. Parlor refuses IP addresses, names that only work inside one network (localhost, .local, .internal and the like), and a user name or password in the address.
- The address is checked again before every delivery, and Parlor will not connect if the name points at a private network address.
- Up to 10 endpoints per venue, and one per address. To change which events an endpoint gets, delete it and add it again (you get a new secret).
- An endpoint belongs to the key that made it. Revoke that key and its endpoints stop too.
Testing
Send a test delivery to get a signed ping at your endpoint whenever you like:
{
"id": "0d9e8f7a-6b5c-4d3e-8f1a-2b3c4d5e6f70",
"event": "ping",
"occurredAt": "2026-10-04T20:40:00.000Z",
"venue": {
"slug": "cotillion"
},
"data": {
"webhookId": "3f1c9a52-8d0e-4b7a-9c61-2e5d7a0b4f18",
"message": "This is a test from Parlor. If your server answered with a 2xx status, it is receiving Parlor's webhooks."
}
}
Verifying signatures
Check every delivery before you trust it. The signature is an HMAC-SHA256, keyed with your endpoint's secret, of the timestamp, a full stop, and the raw body exactly as it arrived. Parse the JSON only after the check: re-encoding it changes the bytes and the signature will not match.
- Split
X-Parlor-Signature at the commas into t and v1.
- Refuse it if
t is more than five minutes from now. That stops an old delivery being replayed at you.
- Compute HMAC-SHA256 of
t + "." + rawBody with the secret, as hex, and compare it to v1 with a constant-time comparison.
Node.js
const crypto = require("node:crypto");
// rawBody: the body exactly as received (a string or a Buffer).
function verifyParlorSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = {};
for (const piece of String(header || "").split(",")) {
const i = piece.indexOf("=");
if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
}
const t = Number(parts.t);
if (!Number.isInteger(t) || !/^[0-9a-f]{64}$/.test(parts.v1 || "")) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1, "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
With Express, take the body raw on that one route:
app.post("/parlor-webhook", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyParlorSignature(req.body, req.get("X-Parlor-Signature"), process.env.PARLOR_WEBHOOK_SECRET);
if (!ok) return res.status(400).send("Bad signature");
res.sendStatus(200); // answer first...
const event = JSON.parse(req.body.toString("utf8"));
handleLater(event); // ...then do the work
});
Python
import hashlib
import hmac
import re
import time
def verify_parlor_signature(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
parts = {}
for piece in (header or "").split(","):
key, sep, value = piece.partition("=")
if sep:
parts[key.strip()] = value.strip()
try:
t = int(parts.get("t", ""))
except ValueError:
return False
if abs(time.time() - t) > tolerance_seconds:
return False
signed = str(t).encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
given = parts.get("v1", "")
# Check the shape first: compare_digest raises on non-ASCII text, and a
# garbled header must be a plain "no", not a crash.
if not re.fullmatch(r"[0-9a-f]{64}", given):
return False
return hmac.compare_digest(expected.encode(), given.encode())
With Flask, request.get_data() is the raw body: verify_parlor_signature(request.get_data(), request.headers.get("X-Parlor-Signature", ""), SECRET).
Versions and changes
This is version 1, and every address carries it: /api/v1/.
- Within version 1, nothing you rely on is taken away or changes meaning. Fields keep their names and types, endpoints stay where they are, and refusals keep their status codes.
- Things are added without notice: new fields in objects, new endpoints, new events, new
reason codes. Write your code to ignore what it does not know.
- A change that would break working code goes into a new version at a new address. When there is one, venues with API keys are told by email, and version 1 keeps working for at least 12 months after that.
- A security or legal problem can force a faster change. If it ever does, the owner hears from us directly.
Guest data
The API hands you the venue's guests' names, email addresses, phone numbers and booking history. The venue stays responsible for that data once it reaches your system, and so do you. In short: keep keys secret, keep the data safe, use it only for the venue's own business, never sell it, honour unsubscribes (a subscriber who left the list must not be emailed from your system either), and delete it when the venue asks. The full wording is in the API terms.
How to get access
API access is by request, and Parlor approves each venue by hand. It is for venues on the Pro or Group plan, and for venues on an older Parlor arrangement that already includes Zapier.
- The venue's owner signs in to Parlor and opens Settings → Integrations & API.
- Under API, they press Request API access and write a short paragraph: what will be built, who is building it, and what it will do with guest data.
- Parlor reads it and approves or declines it; the owner sees the answer in the same place. Once approved, the owner can create keys straight away.
Parlor can withdraw access if the terms are broken or a key is misused. Every API key for the venue is then revoked for good and answers 401, and its webhook endpoints stop; if access is given again later, the owner makes new keys. Zapier needs no request: it works on its own keys for every Pro and Group venue.
Building for a venue and have a question first? Email [email protected].