Calendar booking
Public calendar links let visitors read a published service, choose an available
time and answer its questions. They do not require a login. Configuration of
company identity, theme and publication happens in the Hub Google Calendar tool.
The read-only avatarUrl points to the connected account's available profile
picture. New connections import the image into managed storage. Follow the
returned URL; the public avatar route redirects to a short-lived signed image
URL and uses Cache-Control: no-store. Do not persist the redirect destination.
A missing picture does not prevent booking. Existing connections can retain a
legacy Google image URL until the account is reconnected.
Pages expose defaultLocale and optional localized content for pt-BR,
en-US and es. Prefer the visitor's selected language, then saved preference,
then a supported browser language and the page default. Owner-authored text
falls back to the default locale and base text; no automatic translation is
implied. Built-in interface messages use the visitor's language when there is
no owner override. Translate question labels and option labels for display,
but submit the original question IDs and option values. Changing language must
not change a pending booking's payload or operation ID.
The public paths are /v1/calendar/{pageSlug} and
/v1/calendar/{pageSlug}/{serviceSlug}. Read .../availability with from and to
timestamps containing explicit UTC offsets. The interval is positive, at most
31 days, and excludes to. Display the returned instants in the visitor's chosen
timezone; availability follows the business's scheduling timezone and policies.
A valid range outside the booking horizon returns an empty slots array. When
splitting a month into multiple valid ranges, query them sequentially; a
concurrent request may return 409 APPOINTMENT_CALENDAR_BUSY. Retry briefly
with bounded backoff. A failed query is not evidence that no times are free.
For example, after replacing the published slugs:
curl -G 'https://app.chatt2.me/v1/calendar/example-company/consultation/availability' \
--data-urlencode 'from=2026-10-12T00:00:00-03:00' \
--data-urlencode 'to=2026-10-13T00:00:00-03:00'
Discover IDs and create a booking
Read GET /v1/calendar/{pageSlug} to choose a published service, then
GET /v1/calendar/{pageSlug}/{serviceSlug} for its details. The latter returns
page and service; use service.id as serviceId and service.questions for
question IDs/types/options. Obtain slugs from the published link in the Hub
dashboard. For authenticated creation, get workspaceId from
workspace discovery and use the
service configured in that workspace. For a service without a public link, the
workspace owner supplies its ID from the dashboard. A service ID is separate
from its display name or URL slug.
Create a public booking with POST /v1/calendar/{pageSlug}/{serviceSlug}/appointments.
Replace the slugs, choose startIso from availability, and submit every required
question. This complete example assumes the service has one required short-text
question with the displayed ID:
curl -X POST 'https://app.chatt2.me/v1/calendar/example-company/consultation/appointments' \
-H 'Content-Type: application/json' \
-d '{
"operationId": "7b403e80-aea5-4bc4-a7d5-7dedf5129ed1",
"startIso": "2026-10-12T10:00:00-03:00",
"attendeeName": "Alex Example",
"attendeeEmail": "[email protected]",
"answers": [
{"questionId":"9596d7cc-9dfb-47d9-ad9d-0ba360fa7e91","value":"Discuss account setup"}
]
}'
Authenticated creation requires the same booking fields plus serviceId:
curl -X POST 'https://app.chatt2.me/v1/agent/workspaces/WORKSPACE_UUID/appointments' \
-H 'x-api-key: YOUR_APPLICATION_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"operationId": "8a54af48-77a9-4f38-a04c-d62cdeee8ce2",
"serviceId": "55d07ea3-4b7a-46a1-9ed4-a59a901853d4",
"startIso": "2026-10-12T10:00:00-03:00",
"attendeeName": "Alex Example",
"attendeeEmail": "[email protected]",
"answers": [
{"questionId":"9596d7cc-9dfb-47d9-ad9d-0ba360fa7e91","value":"Discuss account setup"}
]
}'
Send answers: [] when there are no required questions and no optional answers.
operationId must be a random UUID v4. Retain it with the exact payload and reuse
both if a creation response is uncertain. A changed payload with that ID returns
409 APPOINTMENT_IDEMPOTENCY_CONFLICT. A time occupied since the availability
request returns 409 APPOINTMENT_SLOT_UNAVAILABLE; use any returned
alternatives or query availability again. Successful creation returns 201
with appointmentId, service identity, status, timezone, any reserved start
and end, and version. Authenticated responses also include source, attendee
fields, answers, meetingUrl and expiresAt.
Validate question answers
Submit original IDs and option values even if labels were localized. Each answer
has questionId and value; omit an optional unanswered question entirely.
An unknown question, repeated question ID, missing required answer or invalid
value returns 400 APPOINTMENT_ANSWERS_INVALID.
| Question type | Accepted value |
|---|---|
short_text | Nonempty string after trim, at most 500 characters. |
long_text | Nonempty string after trim, at most 2000 characters. |
single_choice | One original option string, matched after trim. |
multiple_choice | Nonempty array of original option strings; no duplicates after trim. |
email | Valid email string, trimmed and lowercased, at most 320 characters. |
phone | A possible phone number; formatting is removed and the country code is normalized. Prefer a string with an explicit international prefix, such as +5511993986082. |
For example, these are complete answer entries for six questions whose IDs and options must match the service's returned configuration, in the table's type order:
[
{"questionId":"9596d7cc-9dfb-47d9-ad9d-0ba360fa7e91","value":"Account setup"},
{"questionId":"9a8a6d87-ff62-4611-9a3b-cb62bd6b536c","value":"We need help setting up a new support team."},
{"questionId":"075e39ec-bfa2-446b-b04a-4896216825ac","value":"Business"},
{"questionId":"a06f959b-066a-4ead-a1c5-cd024fd9fb1c","value":["WhatsApp","Web Chatt"]},
{"questionId":"32b37b0b-aed4-4d62-b194-ae35d9ffcb57","value":"+5511993986082"}
]
attendeeName is a nonempty string up to 200 characters; attendeeEmail is a
valid email up to 320 characters. They are required in addition to any service
questions. startIso must contain an explicit UTC offset or Z.
States, approval and expiry
| Status | Meaning and next actions |
|---|---|
pending_confirmation | Approval required; reserves the time until confirmed, rejected, alternatives requested or the reservation expires. confirm moves it to confirmed; cancel moves it to rejected. |
awaiting_customer | Staff requested alternatives; the previous reservation is released while waiting for the customer's new choice. cancel moves it to rejected. |
confirmed | Meeting confirmed, either automatically or by staff. Updating can reschedule; cancel moves it to cancelled. |
cancelled | A confirmed meeting was cancelled. |
rejected | An unconfirmed request was declined or cancelled. |
expired | An unconfirmed reservation exceeded its review deadline and released its time. |
Automatic bookings become confirmed; approval-required bookings become
pending_confirmation. Receiving a pending response does not mean the meeting
is confirmed. The Google invitation is sent when confirmation is performed and
invitations are enabled. Pending reservations expire at their configured review
deadline; expiry processing clears their reserved time and cancels outstanding
reminders. Google reconciliation can also cancel a confirmed appointment or
reject a pending one when the appointment's own attendee declines the linked
invitation. Another participant's RSVP does not make that attendee decision.
Accepting an invitation or moving its Google event does not approve a pending
request.
Read and mutate appointments
An application API key can read a paginated list with
GET /v1/agent/workspaces/{workspaceId}/appointments, or read one with
GET /v1/agent/appointments/{appointmentId}. Save the current version returned
by GET before changing it. Unknown and cross-application IDs return the same
not-found response.
curl -G 'https://app.chatt2.me/v1/agent/workspaces/WORKSPACE_UUID/appointments' \
-H 'x-api-key: YOUR_APPLICATION_API_KEY' \
--data-urlencode 'status=pending_confirmation' \
--data-urlencode 'limit=25' \
--data-urlencode 'offset=0'
curl 'https://app.chatt2.me/v1/agent/appointments/APPOINTMENT_UUID' \
-H 'x-api-key: YOUR_APPLICATION_API_KEY'
Use a new random UUID for Idempotency-Key for every new action across the
application, including actions on different appointments. Keep it with the
appointment ID, action and body. Retry the same target, action and body with the
same key to replay the original response. Reusing the key for a different target,
action or body returns 400 INVALID_APPOINTMENT_ACTION_PAYLOAD; it must not be
used as a per-appointment sequence number. A missing/blank key or one over 255
characters returns 400 IDEMPOTENCY_KEY_REQUIRED.
Edit a confirmed or pending booking with PATCH and {version,payload}:
curl -X PATCH 'https://app.chatt2.me/v1/agent/appointments/APPOINTMENT_UUID' \
-H 'x-api-key: YOUR_APPLICATION_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 11503546-fbd3-43ed-a515-fd68bd1740d4' \
-d '{"version":1,"payload":{"startIso":"2026-10-12T11:00:00-03:00","attendeeName":"Alex Example"}}'
The payload can update serviceId, reason, startIso, durationMinutes,
location, attendeeName, attendeeEmail and answers; omitted fields retain
existing values. Submitted answers replace the answer collection and must
satisfy the selected service's questions. Unknown payload fields are rejected.
Approve a pending reservation using staff POST with {action,version,payload}:
curl -X POST 'https://app.chatt2.me/v1/agent/appointments/APPOINTMENT_UUID' \
-H 'x-api-key: YOUR_APPLICATION_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 2e45cbaf-43f4-43b1-83e4-58a352499582' \
-d '{"action":"confirm","version":1,"payload":{}}'
Other staff actions are request_alternatives, cancel, takeover and update.
For example, requesting alternatives uses this complete body:
{"action":"request_alternatives","version":1,"payload":{"message":"Please choose another available time."}}
Only confirm, request_alternatives, cancel and takeover accept a custom
message (nonempty, at most 2000 characters). takeover preserves the booking's
state and transfers its linked conversation when one exists; it cannot attach a
conversation to a public/API booking. See the staff action reference
for each action's permitted payload fields.
Cancel with DELETE and a JSON body containing the current version. This
preserves the audit record and returns JSON, rather than an empty 204:
curl -X DELETE 'https://app.chatt2.me/v1/agent/appointments/APPOINTMENT_UUID' \
-H 'x-api-key: YOUR_APPLICATION_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: f5db7ec9-39c8-435b-965d-828848f7f2df' \
-d '{"version":2}'
These mutation examples are independent: always replace their sample version
with the latest GET version. Successful mutations return 200 with
appointment and messageDelivery. For example, confirming an API booking can
return:
{
"appointment": {
"id": "8367696a-c08a-4621-9465-5545b74bcc06",
"status": "confirmed",
"service": {"id":"55d07ea3-4b7a-46a1-9ed4-a59a901853d4","name":"Consultation"},
"start": "2026-10-12T13:00:00.000Z",
"end": "2026-10-12T13:30:00.000Z",
"timezone": "America/Sao_Paulo",
"location": null,
"reason": null,
"version": 2
},
"messageDelivery": {"delivered":false,"error":"CUSTOMER_CHANNEL_NOT_APPLICABLE"}
}
A committed change can succeed even if its channel notice is not delivered;
inspect messageDelivery separately. For public/API bookings,
CUSTOMER_CHANNEL_NOT_APPLICABLE is expected because no chat is attached.
400 APPOINTMENT_VERSION_CONFLICT means the version is stale;
400 APPOINTMENT_STATE_CONFLICT means the action is unsuitable for the current
state. Mutation slot conflicts also use 400 APPOINTMENT_SLOT_UNAVAILABLE;
these differ from the 409 creation/availability conflicts. Reload before
sending a revised action with a new key. A busy calendar lock in a mutation returns
400 APPOINTMENT_CALENDAR_BUSY; retry the unchanged action with bounded backoff.
Google changes to linked events are reconciled asynchronously when automatic synchronization is enabled, with recovery for missed notifications, and refreshed on reads. An external change can advance the appointment version; reload before sending another mutation. Moving a meeting in Google does not approve a pending request, and accepting an invitation is not an approval action. Arbitrary Google events block availability but are not exposed as application appointments. A removed linked event cancels a confirmed booking or rejects a pending request.
Public/API bookings have no conversation automatically attached. Conversation takeover does not apply to them. Use the appointment API and Google Calendar to follow these bookings; do not associate an unverified email address with a chat visitor. Channel message delivery is reported as not applicable for these sources.