Recipes

Recipe: book a meeting from a reply

Use the booking API to offer open Cal.com or Calendly times in a reply, detect the prospect's pick and book it, step by step with exact routes and bodies.

Booking a meeting from a reply over the API takes four calls. Read open times, create an offer that returns a text block, send that block in your reply, then book the slot the prospect picks with POST /booking/offers/:id/book. The scheduling provider sends the invite to the contact's stored address, never to an address taken from an email.

Calendar booking in AY Inbox means offering real open times from your own Cal.com or Calendly account and booking the prospect's choice through that provider. AY Inbox never writes to your calendar: the provider applies your availability rules, refuses conflicts, attaches the video link and sends the invite. The product side, including draft mode and follow-ups, is in the booking guide. The route reference is calendar booking.

Before you start

You needDetail
Booking on the instanceEvery route answers 503 booking_disabled while AYINBOX_BOOKING is off
A booking credentialA Cal.com API key or Calendly personal access token pasted on the connection. It is checked, sealed and never returned
A usable booking typeGET /booking/profiles lists types; use one with status: "ok". Event types that need host confirmation, email verification, a required question, a phone number, seats or an invitee location are refused
A planOn the hosted service, a plan that includes the product; an offer stops with plan_lapsed without one
A thread with a contactThe thread's counterpart must be a contact in the workspace, or the offer answers 400 booking_contact_mismatch

Draft mode (PUT /booking/settings with {"mode": "draft"}) is what lets AY Inbox prepare offers on its own, as drafts, when a reply asks for a call. You do not need it for this recipe: an offer you create over the API stays open when the mode is off.

CallKey scope
Read availabilityREAD_CONTACTS, and READ_UNIBOX with thread_id
Create an offer, stop or resume itWRITE_UNIBOX
Send the replyWRITE_UNIBOX
Read an offerREAD_UNIBOX
BookWRITE_CONTACTS and WRITE_UNIBOX

1. Read availability

curl -s "https://api.ayinbox.com/v1/booking/availability?thread_id=THREAD_ID&limit=10" \
  -H "Authorization: Bearer aybox_..."
{
  "profile_id": "…",
  "timezone": "Europe/Berlin",
  "timezone_source": "contact_field",
  "slots": [
    { "start_at": "2026-11-10T09:00:00Z", "end_at": "2026-11-10T09:30:00Z", "label": "Tue 10 Nov, 10:00" }
  ]
}

Times are read live from the provider with the booking type's minimum notice applied. limit defaults to 20 and goes up to 50. Pass profile_id to use a type other than the default, and timezone (an IANA zone) to label the times yourself.

timezone_source says where the zone came from. With a thread, the zone is resolved in this order: a zone the prospect stated (stated), one a teammate set (member), the contact's timezone field (contact_field), the offset in their email's Date header (date_header), their email domain's country (email_domain), then the campaign's or workspace's zone (host). The last three are guesses, and an offer built on one says so and asks the prospect to correct it.

2. Create the offer

curl -s -X POST "https://api.ayinbox.com/v1/booking/offers" \
  -H "Authorization: Bearer aybox_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: offer-THREAD_ID-1" \
  -d '{"thread_id": "THREAD_ID", "slot_starts": ["2026-11-10T09:00:00Z", "2026-11-10T14:30:00Z"]}'

The body is {thread_id, profile_id?, timezone?, slot_starts?}. Leave slot_starts out and AY Inbox picks the times by the booking type's rules (3 per offer by default, at most 5). Every start you name must be open on the calendar, or the call answers 400 booking_slot_not_available.

The answer is {offer, block, slots}. block is the text to put in your reply, with the times numbered and the zone named in words. This call sends nothing. Only one open offer may hold a thread or a contact; a second answers 409 booking_offer_open.

3. Send it

Put the block in a reply on the same thread with POST /unibox/reply, from the offer's email_account_id:

{
  "email_account_id": "OFFER_EMAIL_ACCOUNT_ID",
  "thread_id": "THREAD_ID",
  "to": ["[email protected]"],
  "subject": "Re: Quick question",
  "body_plain": "Hi Lea, happy to.\n\nBLOCK_TEXT"
}

Keep the block intact. The offer moves to offered only when the sent reply carries every time in it. This is a real send, so a key with WRITE_UNIBOX puts mail on the wire.

4. Read the pick

When the prospect answers, AY Inbox reads the reply against the offered times. Poll GET /booking/offers/:id, or list offers waiting on you with GET /booking/offers?status=pick_pending,needs_human.

GET /booking/offers/:id returns {offer, slots, events}:

  • offer.status is pick_pending when a pick was detected, with candidate_slot_id naming the slot. It is needs_human with a needs_human_reason when the reply was unclear.
  • Each entry in events has a kind (inbound, pick_detected, slot_taken, booked and others), decided_by (deterministic, typed, model, member or api), a confidence and evidence: the words the decision rests on.

A clear pick is "option 2", "the second one" or a full day and time naming one slot with a positive word. A day alone or a time alone is a partial pick that points at one slot. A condition ("option 2 if it's still open") or a negation ("Tuesday I'm out") goes to a person. Read evidence before you book on a pick your code did not see.

5. Book

curl -s -X POST "https://api.ayinbox.com/v1/booking/offers/OFFER_ID/book" \
  -H "Authorization: Bearer aybox_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: book-OFFER_ID" \
  -d '{"slot_id": "CANDIDATE_SLOT_ID", "send_confirmation": true}'

Send {slot_id} for an offered time, or {start_at} for a time the prospect proposed, which must be open on the calendar. Before posting anything, AY Inbox checks the suppression list, the contact's subscription and whether the contact already has an upcoming meeting. send_confirmation defaults to true and sends a short confirmation in the thread; if that send fails, the confirmation waits as a draft. With false the meeting is booked and no confirmation is sent.

AnswerMeaning
201 {meeting, offer, confirmation}Booked. meeting has scheduled_for and join_url
202 {offer, outcome: "unknown"}The provider's answer is being confirmed by lookup. Nothing is posted again; read the offer later
409 booking_slot_takenThe time is gone. alternatives lists open times, already written into a new draft
409 booking_new_replyA reply on the offer has not been read yet; message_id names it. Read it first
409 booking_attempt_in_flightAnother booking on this offer is being confirmed
409 booking_already_bookedThe contact already has an upcoming meeting
400 booking_slot_not_availableThe slot is not on the offer, or the time is not open on the calendar
400 recipient_suppressedThe contact is suppressed or unsubscribed

Booking attempts are unique per offer and time, so a retry, with the same Idempotency-Key or without one, never books the same time twice. The full list is under calendar booking refusals.

Why booking is not an agent tool

The tool registry has a book_meeting tool, but it is send-class: the provider emails an invitation to a real person. Send-class tools are never listed or callable on the MCP server or the REST tool surface. There, an agent can call get_availability, offer_meeting_times, get_meeting_offer and stop_meeting_offer, and then books with POST /booking/offers/:id/book and a key that holds WRITE_CONTACTS and WRITE_UNIBOX. In the dashboard assistant, book_meeting exists and asks a person every time.

The same flow from a terminal:

ayinbox booking availability --thread THREAD_ID
ayinbox booking offer-create --thread THREAD_ID
ayinbox inbox reply ...            # with the block in the body
ayinbox booking offer OFFER_ID     # read the pick and its evidence
ayinbox booking book OFFER_ID --slot SLOT_ID --idempotency-key book-OFFER_ID

booking book prompts before it runs, like every command that sends. Why booking from replies works this way, and what to automate, is covered in booking meetings from cold email replies.

Frequently asked questions

Can I book a Calendly or Cal.com meeting from an email reply over an API?

Yes. Read open times with GET /booking/availability, create an offer with POST /booking/offers, send its block in a reply, then book the prospect's pick with POST /booking/offers/:id/book. The provider sends the invite.

Who receives the calendar invite?

Always the contact's stored email address. Nothing in an email can change the attendee, the event type or the mailbox, and a pick only counts when it comes from the contact's own address.

Does creating an offer send anything?

No. POST /booking/offers returns a text block and sends nothing. The offer becomes offered only when a reply carrying every time in the block is sent.

Why can't an AI agent book through the MCP or tool API?

book_meeting is send-class, because the scheduling provider emails a real person. It is never exposed on the MCP or REST tool surfaces; an agent books with POST /booking/offers/:id/book and a key that holds the scopes.

What happens if the time was taken before I booked it?

The call answers 409 booking_slot_taken with alternatives, open times that are already written into a new draft on the thread. Nothing is booked.

On this page