Skip to main content

iClosed Public API

Unlock the full power of iClosed by integrating our high-ticket scheduling and lead qualification engine directly with your tech stack

Updated today

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:

  1. An iClosed.io account (sign up here).

  2. Business or Enterprise subscription plan.

  3. 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:

  1. Log into iClosed and head to the Settings - Developer page

  2. 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

  1. Keep keys secret – Do not commit API keys to version control or expose them in client-side code (e.g. browser or mobile app).

  2. Use environment variables – Store keys in env vars or a secrets manager and inject them at runtime.

  3. Restrict scope – Use keys with the minimum permissions needed; create separate keys per environment or integration if possible.

  4. Rotate regularly – Rotate keys periodically and after any suspected exposure.

  5. 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

  1. Get an API key

  2. Get the event linkPrefix (username/event-name)

  3. Create (or upsert) a contact

  4. Insert invitee answers (primary invitee questions + secondary / custom fields)

  5. Use the response to disqualify the contact when needed, or to narrow hosts with conditionalUsers

  6. Fetch availabilities (optionally constrained to conditionalUsers)

  7. Create the event call using an exact available dateTime (pass the same conditionalUsers when routing applies)


Detailed 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:

  • 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

{
"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 questionsinviteeQuestionAnswers: each item has type and answer. Allowed type values in the API are EMAIL, PHONE_NO, NAME, FIRST_NAME, and LAST_NAME. Each type must match an invitee question of that kind on the event (see your event setup in iClosed, or GET /v1/events/detail for context).

  • Secondary (custom) questionssecondaryQuestionsAnswer: each item has answer (always an array of strings) and either customFieldId or identifier (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)

  • timeZone and currentDate when 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 in data.conditionalUsers from 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:

  • dateTime

  • timeZone

  • linkPrefix (or eventId)

  • secondaryQuestionsAnswer (may be [])

And one of:

  • contactId, or

  • contact details (firstName, lastName, and email/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/eventDates with conditionalUsers set to a single host id returned 201 with a normal availabilities payload.

  • PUT /v1/contacts with { "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?

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?

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.


Did this answer your question?