Guides
CRM
The CRM API behind the SuperCool Zapier app. Contacts, tags, opportunities, tasks, workflows, texts and emails, REST hook triggers, and the Lead API.
The SuperCool CRM has its own API. The SuperCool app on Zapier is built on it, and your code can call it too: create and update contacts, tag them, open and move opportunities, add tasks, put contacts into workflows, send texts and emails, subscribe to CRM events, and post leads from your own forms.
| Details | |
|---|---|
| Base URL | https://api.supercool.com/api/v1/crm-app |
| Auth | Authorization: Bearer sc_key_…, plus X-Workspace-Id for a team's CRM (Authentication) |
| Format | JSON requests and responses |
| Errors | {"detail": …} with a matching HTTP status (Errors) |
| Rate limits | 300 requests per minute per key on /zapier/…, 600 leads per minute per key on /leads (Rate limits) |
These endpoints are not under /v1. They use the same API keys as the rest of the SuperCool API, but their own base URL and error shape. They're also in the API reference and the OpenAPI spec.
#Authentication
Send one of your dashboard API keys as a bearer token:
Authorization: Bearer sc_key_9fK2...x7Qa
X-Workspace-Id: 6650a1b2c3d4e5f6a7b8c9d0- Use a regular key. An isolated key gets
403. - Pick the CRM. Without
X-Workspace-Id, requests go to the key owner's personal CRM. To use a team's CRM, send the team id inX-Workspace-Id. The key's owner must be a member of that team (any role). The team id is shown on the Zapier card in the CRM's Settings → Integrations. The/zapier/…endpoints also accept it as?workspace_id=, andPOST /leadsasworkspace_idin the body; the header wins. - Everything is done as the key. Contacts, tags, opportunities and tasks it creates, and the activity it writes on contacts, are attributed to the key (
api_key:<key id>).
#Test a key
GET /zapier/me checks the key and returns who it belongs to and which CRM it reaches. Zapier calls it when you connect an account and shows label as the connection's name.
curl https://api.supercool.com/api/v1/crm-app/zapier/me \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "X-Workspace-Id: 6650a1b2c3d4e5f6a7b8c9d0"{
"ok": true,
"user_id": "64f1c2a9e4b0a1b2c3d4e5f6",
"email": "[email protected]",
"workspace_id": "6650a1b2c3d4e5f6a7b8c9d0",
"team_name": "Rivera Studio",
"role": "admin",
"key": { "id": "pat:6a1b2c3d4e5f", "name": "Zapier", "hint": "sc_key_9fK2…x7Qa" },
"label": "[email protected] · Rivera Studio"
}workspace_id is the team id (null for a personal CRM), team_name the team's name, and role your role in that CRM: admin or member.
| Status | detail |
Cause |
|---|---|---|
401 |
Invalid API key |
The key is missing, unknown, expired or revoked. The response has a WWW-Authenticate: Bearer header. |
403 |
An isolated API key cannot use the CRM; use a regular key |
An isolated key. On POST /leads: An isolated API key cannot post leads; use a regular key. |
403 |
The key's owner is not a member of that team |
X-Workspace-Id names a team the key's owner doesn't belong to. |
#Errors
Errors have one field, detail. It's usually a sentence:
{ "detail": "Contact not found" }| Status | When |
|---|---|
400 |
The input can't be used: no email or phone, an invalid email or phone number, an unknown trigger, a target_url that isn't a public URL, an unknown pipeline or stage, a contact that already has an open opportunity in that pipeline, an email address another contact already has, nothing to update. detail says which. |
401 |
The API key is missing, unknown, expired or revoked. |
403 |
An isolated key, or a team the key's owner isn't a member of. |
404 |
The contact, opportunity, workflow or subscription isn't in this CRM. |
422 |
The body doesn't match the endpoint's schema (a required field is missing, or a field has the wrong type), or a text or email was refused by the CRM's rules. See below. |
429 |
Too many requests for this key. See Rate limits. |
503 |
The feature isn't available on this account yet (detail names it, for example Pipelines is not available in this build yet). |
There is no 409: conflicts, like a duplicate opportunity or an email address that's already taken, are 400. An unexpected failure is 500 with {"detail": "Something went wrong"}.
A 422 has one of two shapes. A schema error lists the problems:
{ "detail": [{ "type": "missing", "loc": ["body", "title"], "msg": "Field required", "input": {} }] }A refused text or email has a machine-readable reason and a sentence you can show people:
{ "detail": { "reason": "no_consent", "message": "Text campaigns go only to contacts with explicit SMS consent." } }See refusal reasons for every reason.
#Rate limits
| Limit | Value | Applies to |
|---|---|---|
| Zapier endpoints | 300 per minute | Each API key, every /zapier/… request |
| Lead API | 600 per minute | Each API key, POST /leads |
Over the limit, the request gets 429 with {"detail": "Too many Zapier requests per minute for this key"} (or Too many leads per minute for this key). The window is a rolling 60 seconds: wait, then retry. Texts and emails also have daily sending limits; going over them is a refusal, not a 429.
#Contacts in requests
Most actions work on one existing contact. Name it with any of these fields:
| Field | Meaning |
|---|---|
contact_id |
The contact's id (ct_…). |
contact |
One field that takes an id, an email address or a phone number. |
email |
The contact's email address. |
phone |
The contact's phone number. |
The contact is found by contact_id (or contact) as an id, then as an email address (if it contains @) or a phone number, then by email, then by phone. If none matches, the request gets 404 with Contact not found.
#The contact object
Contact actions return the contact. The main fields:
| Field | Type | Description |
|---|---|---|
id |
string | The contact's id (ct_…). |
email |
string or null | Email address. |
phone |
string or null | Phone number in E.164 (+15555550123). |
first_name, last_name, company |
string or null | Name and company. |
address, city, state, postal_code, country, website |
string or null | Address and website, when set. |
display_name |
string | The full name, else the email, else the phone. |
tags |
string[] | Tag ids (tag_…). GET /zapier/tags maps them to names. |
fields |
object | Custom field values by key. GET /zapier/fields lists the keys. |
source |
string | Where the contact came from: zapier for contacts this API created, lead_api for the Lead API, and others such as form, import or manual. |
owner_user_id |
string or null | The team member who owns the contact. |
status |
string | active, unsubscribed, bounced or blocked. |
dnd |
object | Do Not Disturb: email, sms, calls, all (booleans), reason, at. |
consent |
object | email and call, each {status, source, at}. Email consent explicit is what Send Email needs. |
created_at, updated_at, last_activity_at |
string | ISO 8601 UTC. |
Other fields may be present, such as attribution (first and last touch from forms and the Lead API).
#Contacts
#Create or update a contact
POST /zapier/contacts finds the contact by email, then by phone, and updates it, or creates it if there is none. A new contact's source is zapier. Only the non-empty fields you send are written; nothing is blanked.
| Field | Type | Description |
|---|---|---|
email |
string | Email address. email or phone is required. |
phone |
string | Phone number. US numbers without a country code get +1. |
first_name, last_name, company, address, city, state, postal_code, country, website |
string | Contact details. |
owner_user_id |
string | A team member's user id (GET /zapier/team). |
tags |
string[] or string | Tag names to add (a list, or one comma-separated string; up to 50). Missing tags are created. |
fields |
object | Custom field values by key. Unknown keys are ignored. |
consent |
boolean | true records explicit email consent (source zapier). Otherwise email consent is recorded as imported_unknown, unless the contact's is already explicit. |
sms_consent |
boolean | true records explicit SMS consent (source zapier), if the contact has a phone number. |
Explicit consent is never downgraded. Send consent and sms_consent only when the person actually agreed.
curl https://api.supercool.com/api/v1/crm-app/zapier/contacts \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe",
"phone": "(555) 555-0123",
"tags": "lead, webinar",
"consent": true
}'Returns 200 with the contact at the top level, plus ok and created (true if the contact is new):
{
"ok": true,
"created": true,
"id": "ct_8f2a6c1e9b3d5f7a0c4e",
"email": "[email protected]",
"phone": "+15555550123",
"first_name": "Jane",
"last_name": "Doe",
"display_name": "Jane Doe",
"tags": ["tag_1a2b3c4d5e6f7a8b9c0d", "tag_0d9c8b7a6f5e4d3c2b1a"],
"fields": {},
"source": "zapier",
"status": "active",
"dnd": { "email": false, "sms": false, "calls": false, "all": false, "reason": null, "at": null },
"consent": {
"email": { "status": "explicit", "source": "zapier", "at": "2026-10-23T14:02:11.402000Z" },
"call": { "status": "none", "source": null, "at": null }
},
"created_at": "2026-10-23T14:02:11.402000Z",
"updated_at": "2026-10-23T14:02:11.402000Z"
}Errors: 400 (email or phone is required, Email address is not valid, Phone number is not valid, a custom field value of the wrong type), 422 (wrong field types).
#Update a contact
POST /zapier/contacts/update changes an existing contact. Name the contact with the lookup fields, and send the fields to change: the same contact fields as create or update. Only the fields you send change. tags are added to the contact's tags. Custom field keys must exist. consent and sms_consent are ignored here.
An email or phone that you used to find the contact, and that it already has, is not a change.
curl https://api.supercool.com/api/v1/crm-app/zapier/contacts/update \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "contact": "[email protected]", "company": "Example Co", "tags": ["customer"] }'Returns 200 with ok: true and the updated contact at the top level.
Errors: 400 (Nothing to update, Another contact already has that email, Unknown custom field '…', invalid email or phone), 404 (Contact not found).
#Find a contact
GET /zapier/contacts/find looks a contact up by id, contact, email or phone (query parameters, same order as the lookup fields). It never returns 404: no match is an empty list.
curl "https://api.supercool.com/api/v1/crm-app/zapier/contacts/[email protected]" \
-H "Authorization: Bearer $SUPERCOOL_API_KEY"{ "items": [{ "id": "ct_8f2a6c1e9b3d5f7a0c4e", "email": "[email protected]", "first_name": "Jane", "last_name": "Doe" }] }Each item is a full contact (shortened here).
#Add tags
POST /zapier/contacts/tags adds tags to a contact, by name. Missing tags are created. Name the contact with the lookup fields.
| Field | Type | Description |
|---|---|---|
tags |
string[] or string | Required. Tag names (a list or one comma-separated string; up to 50, 80 characters each). |
curl https://api.supercool.com/api/v1/crm-app/zapier/contacts/tags \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "contact_id": "ct_8f2a6c1e9b3d5f7a0c4e", "tags": ["vip", "webinar"] }'Returns 200 with ok: true and the contact. Adding tags the contact already has changes nothing. Errors: 400 (tags is required), 404 (Contact not found).
#Remove tags
POST /zapier/contacts/tags/remove takes tags off a contact. tags takes names (matched ignoring case and extra spaces) or tag ids (tag_…). Tags the contact doesn't have are skipped.
curl https://api.supercool.com/api/v1/crm-app/zapier/contacts/tags/remove \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]", "tags": "webinar" }'Returns 200 with ok: true and the contact. Errors: 400 (tags is required), 404 (Contact not found).
#Opportunities
An opportunity is a potential sale: one contact, in one stage of one pipeline, with a status of open, won, lost or abandoned.
#Create an opportunity
POST /zapier/opportunities opens an opportunity for a contact. Name the contact with the lookup fields.
| Field | Type | Description |
|---|---|---|
pipeline_id or pipeline |
string | The pipeline, by id (pl_…) or name. Neither: the default pipeline. |
stage_id or stage |
string | The stage, by id (st_…) or name (any case). Neither: the pipeline's first stage. |
name |
string | Defaults to the contact's name. |
value |
number or string | A number is in minor units (cents): 150000 is 1,500.00. A string is in major units: "1,500.00" is 1,500.00. |
currency |
string | 3-letter code. Defaults to the CRM's currency. |
owner_user_id |
string | A team member's user id. Ignored if they're not on the team. |
tags |
string[] or string | Tag names. Missing tags are created. |
fields |
object | Opportunity custom field values by key. |
expected_close_at |
string | A date, YYYY-MM-DD. |
allow_duplicate |
boolean | A contact can have one open opportunity per pipeline. true allows another. |
curl https://api.supercool.com/api/v1/crm-app/zapier/opportunities \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]", "pipeline": "Sales", "stage": "Qualified", "value": "1500" }'Returns 201 with the opportunity:
{
"id": "opp_7d8e9f0a1b2c3d4e5f6a",
"pipeline_id": "pl_2b3c4d5e6f7a8b9c0d1e",
"stage_id": "st_5e6f7a8b9c0d1e2f3a4b",
"contact_id": "ct_8f2a6c1e9b3d5f7a0c4e",
"name": "Jane Doe",
"status": "open",
"value": 150000,
"currency": "usd",
"source": "zapier",
"owner_user_id": null,
"tags": [],
"fields": {},
"expected_close_at": null,
"created_at": "2026-10-23T14:05:00.118000Z",
"updated_at": "2026-10-23T14:05:00.118000Z"
}Other fields, such as followers, lost_reason, stage_entered_at and days_in_stage, are included too. value is always in minor units in responses. Responses from these endpoints don't fill in pipeline_name and stage_name; use GET /zapier/pipelines to name the ids.
Errors: 400 (Pipeline not found, No stage '…' in pipeline '…', Jane Doe already has an open opportunity in Sales, Value must be a number), 404 (Contact not found), 503 (pipelines aren't available on the account).
#Move an opportunity
POST /zapier/opportunities/{opportunity_id}/move moves an opportunity to another stage, another pipeline, or another status. Send at least one of:
| Field | Type | Description |
|---|---|---|
stage_id or stage |
string | The new stage, by id or name, in the opportunity's pipeline (or in the new pipeline). |
pipeline_id or pipeline |
string | Move it to this pipeline (by id or name). Without a stage, it goes to that pipeline's first stage. |
status |
string | open, won, lost or abandoned. |
lost_reason |
string | With status: "lost". |
curl https://api.supercool.com/api/v1/crm-app/zapier/opportunities/opp_7d8e9f0a1b2c3d4e5f6a/move \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "stage": "Proposal sent" }'Returns 200 with the opportunity. Errors: 400 (stage, pipeline or status is required, an unknown stage or pipeline, Status must be open, won, lost or abandoned, the contact already has an open opportunity in the new pipeline), 404 (Opportunity not found), 503.
#Tasks
#Create a task
POST /zapier/tasks adds a task, optionally on a contact.
| Field | Type | Description |
|---|---|---|
title |
string | Required. Longer titles are cut to 200 characters. |
description |
string | Details. |
contact_id, contact, email, phone |
string | Optional. The task's contact, found like the lookup fields. If you send one and it doesn't match, the request gets 404. |
opportunity_id |
string | Link the task to an opportunity. |
assigned_to |
string | A team member's user id (GET /zapier/team). |
due_at |
string | An exact ISO 8601 time. Wins over due_date. |
due_date |
string | YYYY-MM-DD. Without due_time, the task is due all day. |
due_time |
string | HH:MM, with due_date. |
timezone |
string | The IANA zone due_date and due_time are in, like America/Chicago. Defaults to the assignee's zone, else the CRM's, else UTC. |
curl https://api.supercool.com/api/v1/crm-app/zapier/tasks \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Call Jane", "email": "[email protected]", "due_date": "2026-10-27", "due_time": "10:00", "timezone": "America/Chicago" }'Returns 201 with the task: id (tsk_…), title, description_html, contact_id, opportunity_id, assigned_to, due_at, due_date, due_time, all_day, timezone, status (open), origin ({"kind": "manual", "reason": "Created by Zapier"}), created_at and updated_at.
Errors: 400 (title is required, due_date must be YYYY-MM-DD, due_time must be HH:MM, unknown time zone '…'), 404 (Contact not found), 422 (no title field), 503.
#Workflows
#Add a contact to a workflow
POST /zapier/workflows/{workflow_id}/enroll starts a published workflow for a contact. Name the contact with the lookup fields. The workflow's own re-entry rules apply.
curl https://api.supercool.com/api/v1/crm-app/zapier/workflows/wf_0a1b2c3d4e5f6a7b8c9d/enroll \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]" }'{
"ok": true,
"reason": "enrolled",
"enrollment_id": "wfe_6f7a8b9c0d1e2f3a4b5c",
"workflow_id": "wf_0a1b2c3d4e5f6a7b8c9d",
"workflow_name": "New lead follow-up",
"contact_id": "ct_8f2a6c1e9b3d5f7a0c4e"
}Not being enrolled is not an error: the response is 200 with ok: false and the reason:
reason |
Meaning |
|---|---|
enrolled |
The contact is in the workflow (ok: true). |
already_active |
The contact is already going through this workflow. |
no_reentry |
The contact went through it before, and the workflow doesn't allow re-entry. |
not_published |
The workflow isn't published. |
Errors: 404 (Workflow not found, Contact not found), 503.
#Texts and emails
#Consent rules
Send Text and Send Email are marketing sends. They follow the same rules as a campaign, so they only reach people who agreed to hear from you:
- Email needs explicit email consent. The contact's email consent must be
explicit. The API sets it withconsent: trueon create or update or the Lead API. Contacts created without it getimported_unknown, which isn't enough. - Texts need explicit SMS consent. The contact's SMS consent must be
explicit. The API sets it withsms_consent: true, only for a contact with a phone number. Nothing else the API does grants SMS consent. - Opt-outs always win. A contact on Do Not Disturb, an address that unsubscribed, bounced or complained, and a number that replied STOP are never sent to.
- Texts respect quiet hours. Texts go out only during the CRM's texting hours (8:00 to 21:00 by default) in the recipient's time zone, worked out from the number. Outside them, the send is refused with
quiet_hours. - Daily limits apply, and so do pauses on the account (for example after a high bounce rate).
A refused send gets 422 with a reason (see below). Nothing is sent and nothing is queued: fix the cause, or try again later.
Texts and emails use credits. The response's credits is what the send cost.
#Send a text
POST /zapier/messages/sms texts a contact from the CRM's phone number. Name the contact with the lookup fields.
| Field | Type | Description |
|---|---|---|
text (or message) |
string | Required. The text. Longer than 1,600 characters is cut; more than 10 segments, including the opt-out footer the CRM adds when it's needed, is refused with too_long. |
media |
string[] | Optional. One image file already stored in SuperCool (its storage key), up to 1 MB, to send as MMS. Anything else, including a public URL, is refused with media. |
curl https://api.supercool.com/api/v1/crm-app/zapier/messages/sms \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "contact": "+15555550123", "text": "Hi Jane, your order shipped today." }'{
"ok": true,
"contact_id": "ct_8f2a6c1e9b3d5f7a0c4e",
"id": "cmsg_9b2c4d6e8f0a1b3c5d7e",
"status": "accepted",
"kind": "sms",
"conversation_id": "cv_3c4d5e6f7a8b9c0d1e2f",
"segments": 1,
"credits": 10,
"error": null,
"created_at": "2026-10-23T14:10:02.551000Z"
}status is accepted (handed to the carrier), uncertain (the carrier's answer didn't come back), failed, or refused (the carrier says the number opted out). Errors: 400 (text is required), 404 (Contact not found), 422 (refused), 503.
#Send an email
POST /zapier/messages/email emails a contact from the CRM's default sender. Name the contact with the lookup fields.
| Field | Type | Description |
|---|---|---|
subject |
string | Required. Longer subjects are cut to 300 characters. |
html (or body) |
string | The HTML body. html, body or text is required. |
text |
string | A plain-text body. Without html, it's also turned into the HTML body. |
curl https://api.supercool.com/api/v1/crm-app/zapier/messages/email \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]", "subject": "Your order shipped", "text": "Hi Jane,\nyour order shipped today." }'Returns 201 with ok, contact_id and the message (id, status, kind, conversation_id, credits, error, created_at). status is accepted, uncertain or failed. Errors: 400 (subject and html (or text) are required), 404 (Contact not found), 422 (refused), 503.
#Refusal reasons
reason |
Text | Meaning | |
|---|---|---|---|
no_consent |
✓ | ✓ | No explicit consent for this channel. See consent rules. |
dnd |
✓ | ✓ | The contact is on Do Not Disturb. |
suppressed_owner |
✓ | ✓ | The number replied STOP, or the address unsubscribed from your emails. |
suppressed_global |
✓ | ✓ | The number or address can't receive messages (a permanent carrier rejection, a bounce or a complaint). |
paused |
✓ | ✓ | Sending is paused on the account. |
cap |
✓ | ✓ | Today's sending limit is reached. |
credits |
✓ | ✓ | Not enough credits. |
no_phone |
✓ | The contact has no phone number. | |
landline |
✓ | The number is a landline. | |
quiet_hours |
✓ | Outside texting hours for the recipient. | |
no_number |
✓ | The CRM has no phone number to text from. | |
unregistered |
✓ | The number's carrier registration isn't approved yet. | |
suspended, releasing, unpaid |
✓ | The number is suspended, being released, or its monthly charge didn't go through. | |
kill_switch |
✓ | Texting is paused platform-wide. | |
too_long |
✓ | The text is empty or longer than 10 segments. | |
media |
✓ | The attachment isn't usable: one image file stored in SuperCool, up to 1 MB. | |
no_email |
✓ | The contact has no email address. | |
no_channel |
✓ | Email isn't set up yet (no From name and address). | |
unsubscribed, bounced |
✓ | The contact unsubscribed, or their address bounced. |
#Lists for dropdowns
These return what the Zapier app shows in its dropdowns, as {"items": [...]}:
| Endpoint | Items |
|---|---|
GET /zapier/pipelines |
{id, name, stages: [{id, name, position}]}, stages in order. 503 if pipelines aren't available. |
GET /zapier/workflows |
{id, name, status}, published workflows only. 503 if workflows aren't available. |
GET /zapier/tags |
{id, name} |
GET /zapier/team |
{id, name, email, role}: the team's members (or just you, in a personal CRM). |
GET /zapier/fields |
{key, label, type}: the contact custom fields. |
GET /zapier/triggers |
{key, label, noun, types}: the triggers and the CRM event types each fires on. |
curl https://api.supercool.com/api/v1/crm-app/zapier/pipelines \
-H "Authorization: Bearer $SUPERCOOL_API_KEY"{
"items": [{
"id": "pl_2b3c4d5e6f7a8b9c0d1e",
"name": "Sales",
"stages": [
{ "id": "st_4d5e6f7a8b9c0d1e2f3a", "name": "New lead", "position": 0 },
{ "id": "st_5e6f7a8b9c0d1e2f3a4b", "name": "Qualified", "position": 1 }
]
}]
}#Triggers (REST hooks)
To hear about CRM events, subscribe a URL to a trigger. SuperCool POSTs each matching event to it, as it happens. This is how Zapier's instant triggers work, and it works the same for any URL.
#Trigger catalogue
| Trigger key | Fires when | data has |
|---|---|---|
new_contact |
A contact is created (contact.created). |
source, tags (ids), owner_user_id |
contact_updated |
A contact's fields change (contact.updated). |
changed (field names), source |
tag_added |
A tag is added to a contact (contact.tag_added). |
tag_id, tag_name |
tag_removed |
A tag is removed from a contact (contact.tag_removed). |
tag_id, tag_name |
form_submitted |
A CRM form is submitted, or a lead is posted to the Lead API (form.submitted). |
Forms: form_id, form_name, source (form), created, fields, page_url, referrer, utm. Lead API: source (lead_api), lead_source, key_id, created, tags, fields. |
lead_received |
A Facebook lead form is submitted (facebook.lead_received), or a lead is posted to the Lead API. |
Facebook: form_id, form_name, page_id, ad_id, adset_id, campaign_id, leadgen_id, is_test, is_organic and the answers. Lead API: as above. |
appointment_booked |
An appointment is booked (appointment.booked). |
calendar_id, calendar_name, start, end, timezone, assigned_user_id, status, source |
appointment_changed |
An appointment is rescheduled, cancelled or confirmed, or marked showed or no-show (appointment.rescheduled, .cancelled, .confirmed, .showed, .no_show). |
As appointment_booked. A reschedule adds previous_start and previous_end. type says which change. |
opportunity_created |
An opportunity is created (opportunity.created). |
name, pipeline_id, pipeline_name, stage_id, stage_name, status, value (minor units), currency, owner_user_id, source, tags, expected_close_at |
opportunity_stage_changed |
An opportunity moves to another stage (opportunity.stage_changed). |
As opportunity_created, plus previous_stage_id and previous_stage_name |
payment_received |
A payment is received (payment.received). |
source (calendar, site or stripe), amount (minor units), currency, and details from the source |
task_added |
A task is added (task.added). |
title, due_at, due_date, all_day, assignee_id, origin, contact_id, opportunity_id, recurring |
task_completed |
A task is completed (task.completed). |
As task_added |
inbound_webhook |
One of the CRM's inbound webhooks receives a request (webhook.received). |
webhook_id, webhook_name, method, payload (the body, flattened), mapped, created |
A Lead API post fires both form_submitted and lead_received, and new_contact or contact_updated as well.
#Subscribe
POST /zapier/hooks subscribes a URL to one trigger.
| Field | Type | Description |
|---|---|---|
event |
string | Required. A trigger key. |
target_url |
string | Required. A public http or https URL, up to 2,048 characters. Private and local addresses are refused. |
curl https://api.supercool.com/api/v1/crm-app/zapier/hooks \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "event": "new_contact", "target_url": "https://hooks.example.com/supercool/new-contact" }'Returns 201 with the subscription:
{
"id": "zh_4c1d2e3f4a5b6c7d8e9f",
"key_id": "pat:6a1b2c3d4e5f",
"event": "new_contact",
"label": "New Contact",
"target_url": "https://hooks.example.com/supercool/new-contact",
"target_host": "hooks.example.com",
"key_name": "Zapier",
"key_hint": "sc_key_9fK2…x7Qa",
"deliveries": 0,
"failures": 0,
"last_delivery_at": null,
"last_status": null,
"last_error": null,
"created_at": "2026-10-23T13:40:00Z",
"updated_at": "2026-10-23T13:40:00Z"
}- Subscribing the same URL to the same trigger again returns the existing subscription, so retrying a subscribe is safe.
- A CRM can have up to 100 subscriptions. Past that, the request gets
400. - Subscriptions belong to the CRM, not to the key: every key that reaches the CRM sees and can remove them, and they're listed on the Zapier card in Settings → Integrations, where an admin can remove them too.
Errors: 400 (Unknown event '…'; one of: …, target_url must be a public http(s) URL, the 100-subscription limit), 422 (a missing field).
#List subscriptions
GET /zapier/hooks returns the CRM's subscriptions, newest first, as {"items": [...]}. deliveries and failures count delivered and failed events; last_status and last_error describe the latest attempt.
curl https://api.supercool.com/api/v1/crm-app/zapier/hooks -H "Authorization: Bearer $SUPERCOOL_API_KEY"#Unsubscribe
DELETE /zapier/hooks/{hook_id} removes one subscription. Events still waiting to be delivered to it are dropped.
curl -X DELETE https://api.supercool.com/api/v1/crm-app/zapier/hooks/zh_4c1d2e3f4a5b6c7d8e9f \
-H "Authorization: Bearer $SUPERCOOL_API_KEY"{ "ok": true }DELETE /zapier/hooks with a JSON body does the same by id, or by target_url (every subscription to that URL, or only the one for event if you send it). It needs id or target_url (400 otherwise).
curl -X DELETE https://api.supercool.com/api/v1/crm-app/zapier/hooks \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "target_url": "https://hooks.example.com/supercool/new-contact", "event": "new_contact" }'Both return 404 (Hook not found) when nothing matches.
#Sample events
GET /zapier/perform-list/{event} returns up to 3 of the CRM's most recent events for a trigger, newest first, in the same payload a subscription receives. When the CRM has none yet, it returns one made-up sample ("id": "ev_sample") so you can map fields before the first real event. An unknown trigger key gets 400.
curl https://api.supercool.com/api/v1/crm-app/zapier/perform-list/new_contact \
-H "Authorization: Bearer $SUPERCOOL_API_KEY"#Payload
Every delivery is a JSON POST of one event:
{
"id": "ev_3f9a1c2e7b8d4e5f6a7b",
"trigger": "new_contact",
"type": "contact.created",
"label": "Contact created",
"occurred_at": "2026-10-23T14:02:11.402000Z",
"contact_id": "ct_8f2a6c1e9b3d5f7a0c4e",
"contact": {
"id": "ct_8f2a6c1e9b3d5f7a0c4e",
"email": "[email protected]",
"phone": "+15555550123",
"first_name": "Jane",
"last_name": "Doe",
"full_name": "Jane Doe",
"company": "Example Co",
"address": null,
"city": "Austin",
"state": "TX",
"postal_code": "78701",
"country": "US",
"website": null,
"source": "form",
"owner_user_id": null,
"fields": {},
"tag_ids": ["tag_1a2b3c4d5e6f7a8b9c0d"],
"tags": ["lead"],
"dnd": { "email": false, "sms": false, "calls": false, "all": false, "reason": null, "at": null },
"consent": { "email": "explicit", "sms": null },
"created_at": "2026-10-23T14:02:11.402000Z",
"updated_at": "2026-10-23T14:02:11.402000Z"
},
"data": { "source": "form", "tags": ["tag_1a2b3c4d5e6f7a8b9c0d"], "owner_user_id": null },
"ref": {},
"actor": { "kind": "system", "id": null }
}| Field | Description |
|---|---|
id |
The event's id (ev_…). The same on every retry: use it to drop duplicates. |
trigger |
The trigger key the subscription is for. |
type, label |
The CRM event type and its name. |
occurred_at |
When it happened (ISO 8601 UTC). |
contact_id, contact |
The contact it's about, or null. contact.tags are names and tag_ids their ids; consent gives the email and SMS consent status. |
data |
What happened. Its fields depend on the trigger: see the catalogue. |
ref |
Ids of related records, such as opportunity_id, appointment_id, task_id or form_id. |
actor |
Who did it: {kind, id}, where kind is user, api_key, webhook or system. |
#Delivery
The request carries these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
SuperCool-CRM-Zapier/1.0 |
X-SuperCool-Event |
The CRM event type, such as contact.created. |
X-SuperCool-Delivery |
This delivery's id (zhd_…). The same on every retry. |
Deliveries are not signed. Treat the target_url as a secret, and use a URL that's hard to guess.
- Respond with any
2xxwithin 10 seconds. Redirects are not followed. 410 Goneunsubscribes. The subscription is removed, with anything still waiting for it. This is how Zapier turns a hook off when a Zap is deleted.- Some failures are retried. A timeout, a network error,
408,425,429or any5xxis retried after 1 minute, 5 minutes, 15 minutes, 1 hour, 6 hours and 24 hours: up to 7 attempts in all. Then the delivery is marked failed. - Other
4xx(and3xx) responses fail at once, without retries. - Delivery is at least once. Each event gets one delivery per subscription, but that delivery can reach you more than once: for example, when your server handled it but answered after 10 seconds, it's retried. Drop duplicates by
id. Events can arrive out of order. - A revoked key stops its subscriptions. If the API key that created a subscription is revoked, the subscription is removed the next time an event would go to it.
Failures show on the subscription (failures, last_status, last_error) and on the Zapier card in Settings → Integrations.
#Lead API
POST /leads adds a lead to the CRM from your own form, landing page or backend. It creates the contact, or updates the one with the same email or phone, with source lead_api. It's like create or update, plus attribution and an optional opportunity.
| Field | Type | Description |
|---|---|---|
email |
string | Email address. email or phone is required. |
phone |
string | Phone number. |
first_name, last_name, company, address, city, state, postal_code, country, website |
string | Contact details. |
tags |
string[] | Tag names to add (up to 50). Missing tags are created. Must be a list. |
fields |
object | Custom field values by key. Unknown keys are ignored. |
consent |
boolean | true records explicit email consent (source lead_api). Otherwise email consent is imported_unknown, unless it's already explicit. |
sms_consent |
boolean | true records explicit SMS consent, if the lead has a phone number. The only way the Lead API grants SMS consent. |
source |
string | Your label for where the lead came from, like "pricing page". Kept on the contact's activity and the event as lead_source. |
attribution |
object | Optional: utm_source, utm_medium, utm_campaign, utm_term, utm_content, gclid, fbclid, referrer, landing_url. The first attributed lead becomes the contact's first touch; every one updates the last touch. |
opportunity |
object | Optional: add the lead to a pipeline. pipeline_id or pipeline (name), stage_id (default: the first stage), value (minor units if a number, major units if a string), name (default: the contact's name). If the opportunity can't be created (an unknown pipeline, say), the lead is still saved. |
workspace_id |
string | The team id, if you don't send X-Workspace-Id. |
curl https://api.supercool.com/api/v1/crm-app/leads \
-H "Authorization: Bearer $SUPERCOOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"first_name": "Jane",
"phone": "+15555550123",
"tags": ["webinar"],
"consent": true,
"sms_consent": true,
"source": "pricing page",
"attribution": { "utm_source": "google", "utm_campaign": "fall-promo" },
"opportunity": { "pipeline": "Sales", "value": 50000 }
}'Returns 201:
{
"ok": true,
"created": true,
"contact": { "id": "ct_8f2a6c1e9b3d5f7a0c4e", "email": "[email protected]", "phone": "+15555550123", "first_name": "Jane", "source": "lead_api" }
}contact is the full contact (shortened here). Every lead fires the form_submitted and lead_received triggers.
Errors: 400 (email or phone is required, an invalid email or phone), 401, 403, 422 (wrong field types, like tags as a string), 429 (600 leads per minute per key).