iClosed API allows you to connect your software, websites, or third-party tools directly to our lead qualification and scheduling engine.
The API (Application Programming Interface) acts as a bridge, allowing your tech stack to "talk" to iClosed. This is particularly useful for teams who want to build custom booking pages, sync lead data with proprietary CRMs, use AI setter agents or automate event tracking without using webhook or Zapier/Make integrations.
Public API is available on all subscription plans, but the rate limits will differ:
Startup = 20/second
Business = 100/second
Enterprise = 100+ per second
However, if you're a high-volume user and you require a higher API rate throughput, feel free to contact our Customer Support team via in-app chat to discuss an API Limit Increase. Find API docs here.
Getting Started
Here's how to make authenticated requests to the iClosed.io API - docs are here. But before we dive into it, make sure that you're meeting following prerequisites:
An iClosed.io account (sign up here).
Business or Enterprise subscription plan.
An API key (create one in your account settings here).
All API requests require authentication using an API key in the Authorization header. Send your key as:
Authorization: Bearer iclosed_<your-api-key>
Once the request is authenticated, you will be able to use the following supported API methods: GET, POST, PUT and DELETE.
iClosed uses standard HTTP methods to perform different actions:
GET - A "read-only" action and doesn't change your data. Used to retrieve information (e.g., fetching available time slots or viewing contact details).
POST - A "write" action. Used to create data in iClosed (e.g., adding a new contact, initiating a deal, or booking a call).
PUT - An "update" action. Used to update existing records (e.g., rescheduling a call or updating a lead's information).
DELETE - A "delete" action. Used for deleting transactions in iClosed only.
Rate limit is set to 20 requests per second per endpoint on Startup plan and 100 requests per second on Business subscription plan by default for each account.
Authentication
All requests to the iClosed.io API must include a valid API key. Learn all about how authentication works and how to use your key safely.
Overview
Method: Bearer token in the Authorization header
Format: Your API key must be sent with the iclosed_ prefix
Scope: Keys are tied to a user and account; requests run in that context
Header format
Send the API key in every request:
Authorization: Bearer iclosed_<your-api-key>
The word Bearer (with a space) is required.
The token must start with iclosed_ followed by your actual key value.
No other authentication method is supported.
Obtaining an API key
Here're the steps:
Log into iClosed and head to the Settings - Developer page
Create a new API key and copy it immediately (it may not be shown again)
Important Notes
In case you don’t see an API key section or Developers page at all, it may be because your subscription plan doesn't support API keys and webhooks (Startup subscription plan) or your don't have necessary permissions to access the page.
In case you're meeting all requirements, and you don't have access to the Developers page, please contact our Customer Support team immediately.
Key lifecycle
Expiration: Keys can have an expiration date. Expired keys receive 401 Unauthorized.
Revocation: Keys can be revoked in the app. Revoked keys also return 401.
Rotation: Create a new key, switch your integration to it, then revoke or delete the old one.
Example requests
cURL:
curl -X GET "https://public.api.iclosed.io/deals/get" \
-H "Authorization: Bearer iclosed_YOUR_API_KEY" \
-H "Content-Type: application/json"
JavaScript (fetch):
const response = await fetch('https://public.api.iclosed.io/contacts/create', {
method: 'POST',
headers: {
'Authorization': 'Bearer iclosed_YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: 'contact@example.com', firstName: 'Jane' })
});
Python (requests):
import requests
response = requests.get(
'https://public.api.iclosed.io/deals/get',
headers={
'Authorization': 'Bearer iclosed_YOUR_API_KEY',
'Content-Type': 'application/json'
}
)
Error responses
When authentication fails, the API returns 401 Unauthorized with a JSON body such as:
{
"error": "API key is required"
}or
{
"error": "Invalid API key"
}
Most common reasons for errors are:
Response message | Cause |
API key is required | Missing Authorization header or token not starting with iclosed_ |
Invalid API key | Key unknown, revoked, expired, or malformed |
Use the error field in the response to debug authentication issues.
Security best practices
Keep keys secret – Do not commit API keys to version control or expose them in client-side code (e.g. browser or mobile app).
Use environment variables – Store keys in env vars or a secrets manager and inject them at runtime.
Restrict scope – Use keys with the minimum permissions needed; create separate keys per environment or integration if possible.
Rotate regularly – Rotate keys periodically and after any suspected exposure.
Use HTTPS only – Send requests only over HTTPS to the documented base URLs.
Error handling
As far as error handling goes, iClosed API uses standard HTTP status codes:
Code | Description |
200 | Success |
400 | Bad request – invalid parameters or body |
401 | Unauthorized – missing or invalid API key |
404 | Not found |
429 | Too many requests – rate limit exceeded |
5xx | Server error |
Error responses include a JSON body with a message (and sometimes other fields) describing what went wrong.
Endpoints
iClosed provides all necessary endpoints you'd need. Here's a quick reference of main enpoints:
Path | Method | Description |
/contacts/create | POST | Create a contact |
/contacts/update | PUT | Update a contact |
/contacts/delete | POST | Delete contact(s) |
/contacts/createContactNote | POST | Add a note to a contact |
/deals/get | GET | List or get deals |
/deals/create | POST | Create a deal |
/deals/update | PUT | Update a deal |
/deals/delete | POST | Delete deal(s) |
/transactions/get | GET | List or get transactions |
/transactions/create | POST | Create a transaction |
/transactions/update | PUT | Update a transaction |
/transactions/delete | DELETE | Delete a transaction |
/events/timeSlots | GET | Get event time slots |
/events/public/eventDates | POST | Get public event dates |
/events/status | PUT | Update event status |
/eventCalls/public | POST | Create public event call |
/eventCalls/searchEventCall | GET | Search event calls |
/eventCalls/rescheduleCall | PUT | Reschedule a call |
/eventCalls/cancelCall | POST | Cancel a call |
/users/searchUsers | GET | Search users |
/customFields/answerCustomField | POST | Answer custom field |
/customFields/searchContactCustomField | GET | Search contact custom fields |
/callOutcomes/addCallOutcome | POST | Add call outcome |
/callOutcomes/updateCallOutcome | POST | Update call outcome |
/setterClaims/assignSetterOwner | POST | Assign setter owner |
See below detailed API reference.
Contacts
Get Contacts = GET
Create Contact = POST
Update Contact = PUT
Get contact by ID = GET
Get contact notes = GET
Create contact note = POST
Deals
Get deals = GET
Create a new deal = POST
Update a deal = PUT
Events
Get Events = GET
Get Event By ID = GET
Update Event Status = PUT
Troubleshoot Slots = GET
Get Event Date and Time = POST
Products
Get Products = GET
Create a new product = POST
Update a product = PUT
Outcomes
Upsert Call Outcome = POST
Fields
Create a field = POST
Update a field = PUT
Upsert Field Answer = POST
Insert Invitee Answers = POST
Get Contact Stages = GET
Get Object and Fields = GET
Get All Objects and Fields = GET
Calls
Create Call = POST
Get Calls = GET
Reschedule Call = PUT
Cancel Call = PUT
Mark Booked Slot as Free = PUT
Transactions
Get transactions = GET
Create a transaction = POST
Update a transaction = PUT
Delete a transaction = DELETE
User Availabilities
Get user availabilities = GET
Users
List users = GET
Event Call Booking Guide
This guide explains the standard flow about how to book a call via iClosed API - step-by-step. Here's a quick summary:
Overview
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
conditionalUsersFetch availabilities (optionally constrained to
conditionalUsers)Create the event call using an exact available
dateTime(pass the sameconditionalUserswhen routing applies)
Detailed Step-By-Step Guide
Click to see step-by-step guide
Click to see step-by-step guide
1) Get your API key
Navigate to iClosed - Settings - Developers page and create an API key. Then send it in every request as follows:
Authorization: Bearer iclosed_<your-api-key>
2) Get the event link prefix
For booking APIs, use an event linkPrefix in this format:
username/event-name
Example:
iclosedaccountname/discovery-call
Save this value. You will use it for invitee answers, availability lookup, and booking.
3) Create (or upsert) the contact
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.
Request
POST /v1/contacts
Content-Type: application/json
Authorization: Bearer iclosed_<your-api-key>
{
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com"
}Response example
{
"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.
4) Insert invitee answers
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.
Request
POST /v1/fields/inviteeAnswers
Content-Type: application/json
Authorization: Bearer iclosed_<your-api-key>
Response example
{
"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": []
Response (shape)
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.
Troubleshooting
INVALID_INVITEE_QUESTION_TYPE: The event has no invitee question matching the type you sent. Align inviteeQuestionAnswers[].type with 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).
5) Disqualify the contact (after invitee answers)
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).
Request
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.
Response example
{
"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).
6) Fetch available dates/times (with optional conditionalUsers)
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.
Request (no 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"
}Request (after invitee answers returned conditionalUsers: [3684, 3685])
{
"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).
Response example
{
"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).
7) Create the event call
Use the exact available dateTime from the previous step.
Minimum booking fields:
dateTimetimeZonelinkPrefix(oreventId)secondaryQuestionsAnswer(may be[])
And one of:
contactId, orcontact 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.
Request (using contactId + conditionalUsers)
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": []
}Request (upsert contact during booking)
{
"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": []
}Response example
{
"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).
username/event-name
Example:
company/discovery-call
Save this value. You will use it for invitee answers, availability lookup, and booking.
3) Create (or upsert) the contact
Before booking, create a contact with a minimal payload:
firstName
lastName
email or phoneNumber
This endpoint is idempotent for existing records by email/phoneNumber and upserts the same contact when it already exists.
Request
POST /v1/contacts
Content-Type: application/json
Authorization: Bearer iclosed_<your-api-key>
{
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com"
}
Response (example)
End-to-end flow (summary)
Verification notes
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.
FAQs
How are the rate limits managed?
How are the rate limits managed?
Rate limits are configured and enforced by the iClosed team, limited to 20 requests per second with Startup subscription plan and 100 requests per second with Business subscription plan.
However, if you require a higher API rate throughput you can contact our Customer Support team via in-app chat to discuss an API Limit Increase.
What happens if I exceed the rate limit?
What happens if I exceed the rate limit?
If you send too many requests in a short period, the API may respond with 429 Too Many Requests error code.
Back off, retry with exponential delay, or reduce your request rate. For long-term increase, reach out to our customer support team.

