Skip to main content
Version: 1.0

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 typeAccepted value
short_textNonempty string after trim, at most 500 characters.
long_textNonempty string after trim, at most 2000 characters.
single_choiceOne original option string, matched after trim.
multiple_choiceNonempty array of original option strings; no duplicates after trim.
emailValid email string, trimmed and lowercased, at most 320 characters.
phoneA 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":"b8876866-34db-4bff-945b-3e8bcb33b5ec","value":"[email protected]"},
{"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​

StatusMeaning and next actions
pending_confirmationApproval required; reserves the time until confirmed, rejected, alternatives requested or the reservation expires. confirm moves it to confirmed; cancel moves it to rejected.
awaiting_customerStaff requested alternatives; the previous reservation is released while waiting for the customer's new choice. cancel moves it to rejected.
confirmedMeeting confirmed, either automatically or by staff. Updating can reschedule; cancel moves it to cancelled.
cancelledA confirmed meeting was cancelled.
rejectedAn unconfirmed request was declined or cancelled.
expiredAn 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.