This guide shows the standard flow to book a call:
- Get an API key
- Get the event
linkPrefix(username/event-name) - Create (or upsert) a contact
- Insert invitee answers (primary invitee questions + secondary / custom fields)
- Use the response to disqualify the contact when needed, or to narrow hosts with
conditionalUsers - Fetch availabilities (optionally constrained to
conditionalUsers) - Create the event call using an exact available
dateTime(pass the sameconditionalUserswhen routing applies)
Reference: OpenAPI openapi/v1/openapi.json — operations insertInviteeAnswers, updateContact, getPublicEventDates, createEventCall. You can also inspect these endpoints with the iClosed MCP (get-endpoint-info for each path/method).
Create an API key from your iClosed account settings, then send it in every request:
Authorization: Bearer iclosed_<your-api-key>If you have not done this yet, follow the Authentication guide.
For booking APIs, use an event linkPrefix in this format:
username/event-nameExample:
company/discovery-callSave this value. You will use it for invitee answers, availability lookup, and booking.
Before booking, create a contact with a minimal payload:
firstNamelastNameemailorphoneNumber
This endpoint is idempotent for existing records by email/phoneNumber and upserts the same contact when it already exists.
POST /v1/contacts
Content-Type: application/json
Authorization: Bearer iclosed_<your-api-key>{
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com"
}{
"data": {
"contact": {
"id": 139,
"accountId": 4,
"userId": null,
"email": "jane.doe.updated@example.com",
"firstName": "Jane",
"lastName": "Doe",
"status": "QUALIFIED",
"phoneNumber": "+15551234567",
"previewId": "contact_BpK9_yC5DFG7",
"tagId": 7,
"blockedByRecaptcha": false,
"joinedTime": "2026-03-04T20:38:06.409Z",
"country": "US",
"timeZone": "America/New_York",
"ipAddress": "192.168.1.1",
"blockedId": null,
"referrerUrl": "https://example.com/referral",
"createdAt": "2026-03-04T20:38:06.410Z",
"updatedAt": "2026-03-05T09:41:22.614Z",
"deletedAt": null
}
}
}Save data.contact.id as your contactId.
After the contact exists, call POST /v1/fields/inviteeAnswers to submit:
- Primary invitee questions —
inviteeQuestionAnswers: each item hastypeandanswer. Allowedtypevalues in the API areEMAIL,PHONE_NO,NAME,FIRST_NAME, andLAST_NAME. Eachtypemust match an invitee question of that kind on the event (see your event setup in iClosed, orGET /v1/events/detailfor context). - Secondary (custom) questions —
secondaryQuestionsAnswer: each item hasanswer(always an array of strings) and eithercustomFieldIdoridentifier(slug). Use[]if there are none.
linkPrefix and contactId tie answers to the event and contact. You may use previewId instead of contactId when that fits your integration.
POST /v1/fields/inviteeAnswers
Content-Type: application/json
Authorization: Bearer iclosed_<your-api-key>{
"linkPrefix": "company/discovery-call",
"contactId": 139,
"inviteeQuestionAnswers": [
{ "type": "EMAIL", "answer": "jane.doe@example.com" },
{ "type": "PHONE_NO", "answer": "+15551234567" },
{ "type": "FIRST_NAME", "answer": "Jane" },
{ "type": "LAST_NAME", "answer": "Doe" }
],
"secondaryQuestionsAnswer": [
{
"identifier": "company-size",
"answer": ["11-50"]
}
]
}If you have no secondary answers, send an empty array (the field is required):
"secondaryQuestionsAnswer": []On success (201), data includes:
| Field | Meaning |
|---|---|
message | Result message (e.g. confirmation that answers were stored). |
disqualifyingCondition | Array of objects describing disqualification rules that fired, if any. Each entry typically identifies the rule (e.g. id, eventId, disqualificationGroupId), human-readable statement, how it was evaluated (operator, condition, value), and ties back to your event’s disqualification group. Use for logging, UI copy, or redirect URLs alongside isDisqualified. |
conditionalUsers | Array of user IDs (numbers) selected by the event’s conditional routing after evaluating answers. Empty when no conditional hosts apply. |
isDisqualified | true when at least one disqualification condition triggered. |
Example:
{
"data": {
"message": "Questions Answers Inserted Successfully",
"disqualifyingCondition": [],
"conditionalUsers": [1, 2],
"isDisqualified": false
}
}When a disqualification rule matches, isDisqualified is true and disqualifyingCondition lists the rules that matched. Field names and extra keys can vary by event configuration; a typical entry looks like this:
{
"data": {
"message": "Questions Answers Inserted Successfully",
"disqualifyingCondition": [
{
"id": 4,
"eventId": 1,
"statement": "Disqualify",
"operator": "AND",
"value": "Yes",
"condition": "IS",
"disqualificationGroupId": 4
}
],
"conditionalUsers": [],
"isDisqualified": true
}
}In this example, the invitee’s answers satisfied a rule labeled “Disqualify” (e.g. a secondary question answered Yes with condition IS and logical AND within its disqualification group). Your app should treat isDisqualified as the authoritative flag and use disqualifyingCondition for detail when updating CRM, showing messaging, or choosing a redirect.
INVALID_INVITEE_QUESTION_TYPE: The event has no invitee question matching thetypeyou sent. AligninviteeQuestionAnswers[].typewith the invitee questions configured on that event for the public API (the event detail payload may label some fields differently in the UI; the insert endpoint expects the normalized types above).
When isDisqualified is true (and optionally when disqualifyingCondition is non-empty), update the contact’s pipeline status to DISQUALIFIED with PUT /v1/contacts. The request body must include id (the contact id); other fields are optional.
disqualifyingCondition does not perform the status change by itself — your app should decide when to sync CRM state (for example, whenever isDisqualified is true).
PUT /v1/contacts
Content-Type: application/json
Authorization: Bearer iclosed_<your-api-key>{
"id": 139,
"status": "DISQUALIFIED"
}Allowed status values on this endpoint: POTENTIAL, QUALIFIED, DISQUALIFIED.
{
"data": {
"contact": {
"id": 139,
"accountId": 4,
"userId": null,
"email": "jane.doe@example.com",
"firstName": "Jane",
"lastName": "Doe",
"status": "DISQUALIFIED",
"phoneNumber": "+15551234567",
"previewId": "contact_BpK9_yC5DFG7",
"tagId": 7,
"blockedByRecaptcha": false,
"joinedTime": "2026-03-04T20:38:06.409Z",
"country": "US",
"timeZone": "America/New_York",
"blockedId": null,
"referrerUrl": "https://example.com/referral",
"createdAt": "2026-03-04T20:38:06.410Z",
"updatedAt": "2026-03-05T10:00:00.000Z",
"deletedAt": null
}
}
}If the lead is disqualified, stop the booking flow (no need to fetch slots or create a call unless your product allows exceptions).
Call POST /v1/events/eventDates with:
linkPrefix(required)timeZoneandcurrentDatewhen you want a specific anchor (recommended for predictable calendars)conditionalUsers(optional): a comma-separated string of user IDs, e.g."3684"or"1,2". Pass the same IDs returned indata.conditionalUsersfrom Insert invitee answers (join the numeric array into a string). This restricts availability to those hosts so slots reflect conditional routing.
POST /v1/events/eventDates
Content-Type: application/json
Authorization: Bearer iclosed_<your-api-key>{
"linkPrefix": "company/discovery-call",
"timeZone": "America/New_York",
"currentDate": "2026-02-26"
}{
"linkPrefix": "company/discovery-call",
"timeZone": "America/New_York",
"currentDate": "2026-02-26",
"conditionalUsers": "3684,3685"
}In application code, derive the string from the array, for example: conditionalUsers.join(',') (skip the field or pass undefined when the array is empty).
{
"data": {
"isPreview": false,
"availabilities": {
"2026-02-26": ["09:00", "09:15", "10:00"],
"2026-02-27": ["09:00", "14:00"]
}
}
}Pick an available slot and construct an exact booking dateTime in ISO 8601 UTC format (for example, 2026-03-15T14:00:00.000Z).
Use the exact available dateTime from the previous step.
Minimum booking fields:
dateTimetimeZonelinkPrefix(oreventId)secondaryQuestionsAnswer(may be[])
And one of:
contactId, or- contact details (
firstName,lastName, andemail/phoneNumber) to upsert at booking time
conditionalUsers (optional): same semantics as for Get Event Date and Time — a comma-separated string of user IDs. If you computed availabilities with conditional hosts, pass the same conditionalUsers value here so the booking is validated against those users’ calendars and assignment stays consistent.
POST /v1/eventCalls
Content-Type: application/json
Authorization: Bearer iclosed_<your-api-key>{
"linkPrefix": "company/discovery-call",
"contactId": 139,
"dateTime": "2026-03-15T14:00:00.000Z",
"timeZone": "America/New_York",
"conditionalUsers": "3684,3685",
"secondaryQuestionsAnswer": []
}{
"linkPrefix": "company/discovery-call",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"dateTime": "2026-03-15T14:00:00.000Z",
"timeZone": "America/New_York",
"conditionalUsers": "3684",
"secondaryQuestionsAnswer": []
}{
"data": {
"eventCall": {
"message": "Event call created successfully",
"status": 200,
"data": {
"startTime": "2026-03-15T14:00:00.000Z",
"endTime": "2026-03-15T14:15:00.000Z",
"id": 1585584,
"closerId": 3685,
"timeZone": "America/New_York",
"guestEmail": "nayyab142@iclosed.io",
"token": "ULC5lA1vKpS3OLD+m07eOz1U9/mAaXm6Q2j7wLCp1agN53Qt2gYOmbqAx3UQfMmMRQU4FIdSuh0kbIbUJBeBmw==",
"closerName": "zarrar khan",
"previewId": "call_fBdaH9gAZm8h",
"closerEmail": "zarrar1961@gmail.com",
"confirmationLink": "www.google.com"
}
}
}
}Save data.eventCall.data.id and data.eventCall.data.previewId for later operations (search, reschedule, cancel, and tracking).


The following were exercised against the production API with a valid account key:
POST /v1/events/eventDateswithconditionalUsersset to a single host id returned 201 with a normalavailabilitiespayload.PUT /v1/contactswith{ "id", "status": "DISQUALIFIED" }returned 200 and an updated contact.
POST /v1/fields/inviteeAnswers depends on the event having invitee questions whose types match the inviteeQuestionAnswers[].type values you send; otherwise the API returns INVALID_INVITEE_QUESTION_TYPE. Confirm invitee question configuration on the event before relying on this step in production.