Public API reference
IT / TechnicalPreb exposes a small set of public HTTP endpoints under /api/public/*. Most
power the booking widget and are unauthenticated; one (transactions) records
revenue and is authenticated with an API key. All
endpoints return JSON, run over https://app.preb.co, and are
rate limited.
Authentication & signing
Each endpoint uses one of three authentication models:
| Endpoint | Method | Auth |
|---|---|---|
/api/public/capture | POST | None (public widget) |
/api/public/book | POST | None (public widget) |
/api/public/transactions | POST | API key (HMAC-signed) |
/api/public/month-availability | GET | None (public widget) |
/api/public/booking-summary | GET | Signed token |
/api/public/funnel-events | POST | None (public widget) |
Only /api/public/transactions uses an API key. It is signed with HMAC — see
API keys → Authentication for the
exact header format. booking-summary is read with a short-lived signed token
that Preb mints for an existing booking. The remaining endpoints are public
widget endpoints with no credential.
All times are UTC ISO-8601
Timestamps in requests and responses use UTC ISO-8601 (e.g.
2026-06-29T10:00:00Z). Timezones for availability and booking are passed
separately as IANA names (e.g. Europe/Berlin).
POST /api/public/capture
Creates or updates a contact ("lead") from the booking form's first step. Public; rate limited to 20 requests / 60s per IP and event type.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
eventTypeId | UUID | ✅ | The event being booked |
email | string | ✅ | Max 254 chars |
name | string | — | 1–120 chars (omit when empty; don't send "") |
phone | string | — | Max 40 chars |
attributionJsonb | object | — | UTM / click-id attribution payload |
customFields | object | — | Up to 50 keys; serialises to ≤ 8 KB |
step1Complete | boolean | — | true only when the visitor presses Continue; gates qualification + routing |
priorContactId | UUID | — | Used to rename a session's lead in place after a typo correction |
Response 200 — shape varies by outcome:
// normal capture
{ "contactId": "…", "wasCreated": true, "disqualified": false,
"redirectUrl": null, "allowedHostIds": ["…"] }
// disqualified by a rule
{ "contactId": "…", "wasCreated": true, "disqualified": true,
"redirectUrl": "https://…", "fullyBooked": false,
"disqualifiedTitle": "…", "guestMessage": "…",
"ctaLabel": "…", "ctaUrl": "…" }
Notable: first-touch attribution is preserved across updates, custom fields are deep-merged, and unknown custom-field keys are dropped (not an error).
POST /api/public/book
Commits a booking for a chosen slot. Public, but requires an Idempotency-Key
header (8–512 characters) so retries can't double-book. Rate limited to 5
requests / 60s per IP and event type.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
eventTypeId | UUID | ✅ | |
startTimeUtc | string | ✅ | UTC ISO-8601 with offset |
bookerTimezone | string | ✅ | IANA timezone |
attendees | array | ✅ | 1–20 items, each { name, email } |
responses | object | — | Answers to booking-form questions (string → string) |
selectedLocationKind | string | — | The location the booker chose, when the event type offers several. Must match a configured kind. Omit to use the event's first location. |
attendeeAddress | string | — | The booker's meeting address. Required when the chosen location is in_person_attendee (max 500 chars). |
consent | boolean | — | Required when the event type requires consent |
attributionJsonb | object | — | Attribution payload |
contactId | string | — | Links the booking to a known contact |
rescheduledFromUid | string | — | The booking this one replaces |
embed | boolean | — | Suppresses the confirmation redirect inside an iframe |
Response 200
{
"uid": "bk_…",
// present only when a confirmation redirect is configured:
"redirectUrl": "https://…",
"redirectDelaySeconds": 3,
"redirectNewTab": false,
"redirectCountdown": true,
"redirectFireTracking": true
}
A retry with the same Idempotency-Key (or a concurrent duplicate) returns the
existing booking's uid. Host selection, routing enforcement, and calendar sync
all happen server-side; the client's host filter is re-derived from the stored
contact and is not trusted.
POST /api/public/transactions
Records a payment against a contact — the bridge for reporting revenue from an external billing system (Make, Zapier, a custom script). Authenticated with an API key (the only endpoint that is). Rate limited to 1000 requests / hour per key.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | ✅ | Resolves / creates the contact |
amount | number | ✅ | Positive, up to 9999999999.99 |
currency | string | ✅ | 3-letter code (e.g. EUR); stored upper-cased |
transaction_at | string | ✅ | UTC ISO-8601; backfill allowed |
preb_booking_uid | string | — | Attach to a specific booking (max 64) |
description | string | — | Max 2000 chars |
You can also send an optional Idempotency-Key header (must match
[A-Za-z0-9_.:-]{1,255}) to make replays safe.
Response 201
{ "transactionId": "…", "dealId": "…", "contactId": "…" }
A replay of a previously seen idempotency key returns 200 with
"idempotentReplay": true. The transaction is attached to the contact's most
recent open deal where possible, and closes that deal on an exact amount match.
GET /api/public/month-availability
Returns the bookable days in a date range, in the booker's timezone. Public; rate limited to 60 requests / 60s per IP. Used by the widget's calendar to gray out fully-booked days.
Query parameters
| Param | Required | Notes |
|---|---|---|
eventTypeId | ✅ | |
tz | ✅ | IANA timezone |
from | — | YYYY-MM-DD; defaults to today |
to | — | YYYY-MM-DD; defaults to the end of the booking window |
h | — | Restrict to specific routed hosts |
Response 200
{ "availableDates": ["2026-07-01", "2026-07-03"], "noHostsAvailable": false }
A range wider than the booking window (plus a small margin) returns
400 range_too_large.
GET /api/public/booking-summary
Returns a public summary of one booking (used by confirmation / reschedule screens). Authenticated with a short-lived signed token, not an API key — Preb mints the token for a specific booking and includes it where it's needed. Rate limited to 60 requests / 60s per IP.
Request: no body. Send the token in the header:
Authorization: Bearer <token>
Response 200
{
"uid": "bk_…",
"title": "Intro call",
"start_time": "2026-07-01T09:00:00Z",
"end_time": "2026-07-01T09:30:00Z",
"location": { "kind": "google_meet", "conferencingLink": "https://…" },
"host_name": "Jane Host",
"host_username": "jane",
"avatar_url": "https://…",
"attendees": [{ "name": "…", "email": "…", "role": "guest" }],
"is_routed": true
}
This endpoint is intentionally not blocked for workspaces with a lapsed subscription — it's a read of an already-completed booking.
POST /api/public/funnel-events
Reports how long a visitor spent on each booking-form field and whether they abandoned it — the data behind form drop-off analysis. Public; rate limited to 50 requests / 60s per IP and event type.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
eventTypeId | UUID | ✅ | |
events | array | ✅ | 1–20 items |
Each item: { "field": string, "dwell_seconds": number, "abandoned": boolean }
(dwell_seconds 0–3600).
Response 200
{ "inserted": 3 }
Events for unknown fields are dropped silently; { "inserted": 0 } is a normal
response, not an error.
Errors & status codes
Errors are JSON: { "error": "<code>" }, sometimes with an extra message or
issues (validation detail). Common codes:
| Status | Example codes | Meaning |
|---|---|---|
400 | invalid_json, invalid_input, invalid_timezone, missing_idempotency_key, consent_required, range_too_large, phone_required, address_required, invalid_location | Malformed or rejected request |
401 | unauthorized, missing_token, invalid_token | Auth failed (key signature or token) |
403 | email_not_allowed | Email blocked by the event's policy |
404 | event_type_not_found, not_found | Target doesn't exist or is hidden |
409 | booking_unavailable, slot_unavailable, per_invitee_limit | The booking can't proceed right now |
422 | contact_not_found | Couldn't resolve a contact (transactions) |
429 | rate_limited | Over the limit — see Retry-After |
500 | internal, no_location_configured | Server-side error |
See Rate limits & security for the rate headers and the fail-open behaviour.