Move from another tool
GoHighLevel's API on Cook
Keep your GoHighLevel scripts. Point them at Cook's GoHighLevel address with a Cook key, and your contacts, opportunities, conversations, calendars and users answer the way GoHighLevel's API does.
Cook answers GoHighLevel’s API v2 (LeadConnector,
services.leadconnectorhq.com). A script that creates contacts, moves
opportunities, logs messages, books appointments or reads your team keeps
working when you point it at Cook. A GoHighLevel location is your Cook
workspace, contacts are Cook leads, opportunities are a lead’s place in a
pipeline, calendars are Cook booking pages, appointments are Cook meetings and
users are the people on your team.
The switch: two lines
Change the address and the key. Nothing else changes.
Base URL: https://api.trycook.ai/compat/ghl
Authorization: Bearer ck_live_…
Version: 2021-07-28
Make the key in Cook under Settings → API keys. The key decides the workspace, and its permissions decide what the script may do (see the compat overview). Use an account key (one made for a person) for the team calls; see Approvals.
Versionheader. Required, as on GoHighLevel. Cook accepts2021-07-28,2021-04-15,2023-02-21,v3andlegacy. A missing one answers401“version header was not found.”; any other value answers401“version header is invalid”.- Your
locationId. Your workspace has a GoHighLevel-shaped location id (20 letters and digits) and a company id. Read them once withGET /locations/search:locations[0].idis yourlocationId,locations[0].companyIdyourcompanyId. Pass them wherever GoHighLevel asks for them. Another location’s id answers403“The token does not have access to this location.”; another company’s id answers401“The token does not have access to this company.”.
Everything after the address is as GoHighLevel documents it: the paths,
query parameters, request and response bodies, succeded spelled as
GoHighLevel spells it, and the error body
({ "statusCode", "message", "error" }; a 422 carries message as a list
of reasons). A path Cook doesn’t answer is a 404
"Cannot <METHOD> /<path>".
What Cook answers
Supported means it behaves as GoHighLevel documents it. Partial says what differs. Not supported says why.
Locations
| GoHighLevel call | On Cook | Notes |
|---|---|---|
GET /locations/search | Supported | Your one location. email filters on the workspace’s email. |
GET /locations/{locationId} | Partial | Name, email, website, logo and timezone: your default availability schedule’s, else the time zone Cook last saw you chat from, else UTC. settings says one contact per email or phone and one opportunity per contact. |
POST /locations/, PUT and DELETE /locations/{locationId} | Not supported | 404. Make and remove workspaces in Cook. |
/locations/{locationId}/timezones, /templates, /tasks/search, /recurring-tasks | Not supported | 404. |
Contacts
| GoHighLevel call | On Cook | Notes |
|---|---|---|
POST /contacts/ | Supported | 201. Name, email, phone, company, source, timezone, address fields, website, date of birth, tags, dnd (or dndSettings), assignedTo and customFields. Fields Cook has no place for (gender, attribution and the like) are not kept. An email or phone another contact already has answers 400 “This location does not allow duplicated contacts.” with meta.contactId and meta.matchingField, as GoHighLevel does with duplicates off. |
GET, PUT, DELETE /contacts/{contactId} | Supported | Delete removes the lead and what it owns in Cook. |
POST /contacts/upsert | Supported | Matches on email, then phone. new: true when it made one. |
POST /contacts/search | Supported | filters (eq, not_eq, contains, not_contains, exists, not_exists and ranges on dates) on name fields, email, phone, company, source, timezone, type, tags, assignedTo, dnd, dateAdded and customFields.<id>; up to two sort fields; page + pageLimit (up to 500, first 10,000 rows) or searchAfter. |
GET /contacts/search/duplicate | Supported | email or number. { "contact": null } when none. |
GET /contacts/ | Supported | query, limit (up to 100), startAfter, startAfterId. |
/contacts/{contactId}/notes, /notes/{id} | Supported | Notes on the lead’s timeline in Cook. |
/contacts/{contactId}/tasks, /tasks/{taskId}, /tasks/{taskId}/completed | Partial | Tasks on Cook’s task board, linked to the lead. Only tasks made through this API list on a contact (see Gotchas). dueDate keeps the day and reads back at T00:00:00.000Z. |
GET /contacts/{contactId}/appointments | Supported | The lead’s meetings in Cook. |
POST, DELETE /contacts/{contactId}/tags | Supported | |
POST /contacts/bulk/tags/update/{type} | Supported | add or remove (with removeAllTags), up to 500 contacts. |
POST, DELETE /contacts/{contactId}/workflow/{workflowId} | Partial | See Workflows. |
/contacts/{contactId}/campaigns/…, /followers, /contacts/bulk/business, /contacts/business/{businessId} | Not supported | 404. Cook has no campaigns, followers or businesses; a lead has one owner (assignedTo). |
GET /contacts/lookup | Not supported | 404. It is not in GoHighLevel’s published API v2; use GET /contacts/search/duplicate. |
Tags, custom fields and custom values
| GoHighLevel call | On Cook | Notes |
|---|---|---|
GET, POST /locations/{locationId}/tags, GET, PUT, DELETE /locations/{locationId}/tags/{tagId} | Supported | See Tags. |
GET, POST /locations/{locationId}/customFields, GET, PUT, DELETE /customFields/{id} | Partial | Contact fields only. See Custom fields and values. |
POST /locations/{locationId}/customFields/upload | Not supported | 404. |
GET, POST /locations/{locationId}/customValues, GET, PUT, DELETE /customValues/{id} | Supported | See Custom fields and values. |
Opportunities and pipelines
| GoHighLevel call | On Cook | Notes |
|---|---|---|
GET /opportunities/pipelines | Supported | Your Cook pipelines and their stages. |
POST /opportunities/ | Partial | 201. One opportunity per contact: see Opportunities. |
GET, PUT, DELETE /opportunities/{id} | Supported | PUT moves the lead to another stage or pipeline. DELETE takes the lead out of the pipeline; the contact stays. |
PUT /opportunities/{id}/status | Supported | open, won, lost, abandoned. |
POST /opportunities/upsert | Supported | Updates the contact’s opportunity, moving it if pipelineId differs. |
GET /opportunities/search | Supported | location_id, pipeline_id, pipeline_stage_id, contact_id, assigned_to, status, q, date, endDate, order, limit, page, startAfter + startAfterId. |
POST /opportunities/search | Partial | query, limit, page, searchAfter, and filters with eq on pipeline_id, pipeline_stage_id, contact_id, assigned_to, status, id. aggregations is empty. |
| Pipeline create, edit, delete | Not supported | GoHighLevel’s API v2 has no such calls. Make pipelines in Cook under Pipelines, or with Cook’s own API (/v1 crm.pipelineCreate). |
/opportunities/{id}/followers, /opportunities/lost-reason | Not supported | 404. |
Conversations and messages
| GoHighLevel call | On Cook | Notes |
|---|---|---|
POST /conversations/ | Supported | 201. A contact that already has a conversation answers 400 “Conversation already exists” with its conversationId. |
GET, PUT, DELETE /conversations/{conversationId} | Partial | PUT: starred and unreadCount (0 marks it read in Cook too); feedback answers 422. DELETE removes the conversation and its messages. |
GET /conversations/search | Partial | contactId, id, query, assignedTo, sort, startAfterDate, limit (up to 100), lastMessageType, lastMessageDirection, lastMessageAction, status, startDate, endDate. followers, mentions, the score profile filters and any sortBy but last_message_date answer 422. |
GET /conversations/{conversationId}/messages | Supported | { "messages": { "lastMessageId", "nextPage", "messages": [...] } }, as GoHighLevel’s live API answers. limit up to 100, lastMessageId, type. |
GET /conversations/messages/{id} | Supported | Also reads a send that waits for approval. |
POST /conversations/messages | Partial | SMS and Email only. See Sending messages. |
POST /conversations/messages/inbound | Supported | Logs a message the contact sent, without sending anything. See Logging messages. |
POST /conversations/messages/outbound | Supported | Logs a call (type: "Call", as GoHighLevel allows). |
PUT /conversations/messages/{messageId}/status | Partial | For messages Cook’s own carrier didn’t send. A text Cook sent answers 400 “This message’s delivery status is reported by the carrier that sent it”. |
| Attachments, uploads, recordings, transcriptions, export, email scheduling, custom subtypes, unsubscribes, live-chat typing, review replies | Not supported | 404. Cook keeps call recordings and transcripts on the meeting or call in Cook. |
Calendars and appointments
| GoHighLevel call | On Cook | Notes |
|---|---|---|
GET /calendars/ | Supported | Your booking pages. showDrafted=false lists live ones only. groupId answers an empty list. |
POST /calendars/ | Partial | Makes a booking page and publishes it (unless isActive: false) in the location’s timezone. Kept: name, description, eventColor, meetingLocation, slotDuration (+ slotDurationUnit), formSubmitRedirectURL, teamMembers. Other fields are not kept. |
GET /calendars/{calendarId} | Supported | |
PUT /calendars/{calendarId} | Partial | Name, description, length, location, redirect or thank-you message and team members, through the workspace’s approval rule (see Approvals). isActive publishes or unpublishes at once. |
DELETE /calendars/{calendarId} | Supported | A routine’s test page answers 400. |
GET /calendars/{calendarId}/free-slots | Supported | startDate, endDate (ms), timezone, userId (a user who isn’t a host gets no slots). Uses the page’s hours, connected calendar, buffers and notice. |
GET /calendars/events | Supported | locationId, startTime, endTime (required), calendarId, userId. groupId answers an empty list. |
GET /calendars/events/appointments/{eventId} | Supported | |
POST /calendars/events/appointments | Partial | Books through the booking page itself (it must be live, the contact needs an email, the time must be free). ignoreFreeSlotValidation is not honoured. A status other than confirmed, or an endTime that changes the page’s length, answers 422. |
PUT /calendars/events/appointments/{eventId} | Partial | startTime reschedules; appointmentStatus cancelled cancels; showed and noshow set the meeting’s outcome. Title, address, assigned user, description, location, rrule, another calendar or another length answer 422. |
DELETE /calendars/events/{eventId} | Partial | 201 { "succeeded": true }. Cancels the meeting; Cook keeps it, and it reads back as cancelled. |
| Groups, block slots, appointment notes, resources, notifications, schedules | Not supported | 404. |
Workflows and users
| GoHighLevel call | On Cook | Notes |
|---|---|---|
GET /workflows/ | Supported | Your Cook routines. status is published when the routine is on, else draft. |
GET /users/, GET /users/{userId} | Supported | The people on your team. Owners and admins read as admin, members as user. |
GET /users/search | Supported | companyId (yours), query, role, ids, sort, sortDirection, skip, limit. |
POST /users/, PUT, DELETE /users/{userId} | Partial | Through an approval card. See Approvals. |
POST /users/search/filter-by-email | Not supported | 404. Use GET /users/search with query. |
Everything else
Forms and surveys, campaigns, custom objects and associations, invoices,
payments, products, funnels, blogs, social posting, SaaS and agency calls and
the rest of GoHighLevel’s API answer 404. Marketplace OAuth is served; see
Marketplace apps (OAuth).
Opportunities
GoHighLevel keeps opportunities apart from contacts, and a contact can have several. In Cook an opportunity is the lead’s place in a pipeline: its pipeline, stage, status and value. So:
-
One opportunity per contact. A second
POST /opportunities/for the same contact answers400and names the one it has:{ "statusCode": 400, "message": "This contact already has an opportunity in this pipeline.", "error": "OPPORTUNITY_NO_DUPLICATE", "meta": { "existingId": "…" } }In another pipeline the message is “This contact already has an opportunity in another pipeline. Cook keeps one opportunity per contact: move it with PUT /opportunities/{id} (pipelineId, pipelineStageId).”
-
POST /opportunities/upsertupdates the contact’s opportunity, and moves it when you give anotherpipelineId. -
Moving is
PUT /opportunities/{id}withpipelineIdandpipelineStageId. A new pipeline without a stage starts at its first stage. -
Deleting an opportunity takes the lead out of the pipeline. The contact stays.
-
Imported contacts. A GoHighLevel contact you imported with several opportunities is several leads in Cook that share its contact id. Each is its own opportunity, and contact edits reach all of them.
-
The opportunity’s name is kept on the lead. Its custom fields are not: Cook keeps custom fields on contacts (see below).
-
createdAtis when the opportunity was made, not when its contact was: a deal opened today on a contact from last month is today’s deal.GET /opportunities/searchsorts by it (order) and filters on it (date,endDate). An imported opportunity keeps GoHighLevel’s owncreatedAt. -
From an AI agent, the Cook MCP’s
lead_set_opportunity(or/v1crm.setOpportunity) makes and moves the same opportunities.
Approvals
Some calls change things a person in your workspace may want to see first. Cook decides them with your workspace’s approval rules, the same ones that decide what Cook’s AI may do, under Settings → Approvals. When a card is filed, nothing happens until a person approves it in Cook.
-
Sending a message (
POST /conversations/messages) follows the rules for texting and emailing a lead. When a card is filed the answer is still200, withstatus: "pending":{ "conversationId": "…", "messageId": "…", "emailMessageId": "…", "messageIds": ["…"], "msg": "Message is waiting for approval: nothing is sent until a person approves it in Cook.", "status": "pending" }emailMessageIdis there for an Email. Read the message again withGET /conversations/messages/{id}: it readspendingwhile the card waits, then the carrier’s status once it is sent, orfailedwhen the card is turned down, expires or fails. -
Changing a calendar (
PUT /calendars/{calendarId}) follows the rule for booking pages. When a card is filed:403“Cook changes a calendar through an approval card: card … waits for a person in Cook, and nothing changes until it is approved.” When the rule lets the change through, Cook waits for it (up to about 10 seconds) and answers200with the calendar. -
Rescheduling or cancelling an appointment (
PUTandDELETEon events) follows your AI setter’s settings. When they ask for a card:403“Nothing was moved yet: approval card … waits for a person in Cook. …” (or “Nothing was cancelled yet: …”), ending with the note the card carries. -
The team always waits for a person.
POST /users/answers403“Cook does not change the team directly: it filed approval card … (”…”). … is invited when a person approves it in Cook.”PUT /users/{userId}with a newroleanswers403with the same start, ending ”…‘s role changes when a person approves it in Cook.” Other fields (name, email, password, phone, permissions) answer422: people change their own profile in Cook. An owner’s role answers422.DELETE /users/{userId}answers200{ "succeded": false, "message": "Cook does not change the team directly: it filed approval card … (\"…\"). … is removed when a person approves it in Cook." }.- These three need an account key; a workspace key is refused.
Everything else (contacts, notes, tasks, tags, fields, values, opportunities, conversations, logging messages, booking, enrolling in a workflow) applies at once.
Tags
GoHighLevel’s location tags are the tags on your Cook leads.
GET /locations/{locationId}/tagslists every tag in use on a lead, plus the ones made withPOSTand not used yet.PUT /locations/{locationId}/tags/{tagId}renames the tag on every lead that has it.DELETE /locations/{locationId}/tags/{tagId}takes it off every lead.
Custom fields and values
- Custom fields are Cook’s lead fields.
dataTypeTEXT,LARGE_TEXT,PHONEandTEXTBOX_LISTare text;NUMERICALandMONETORYare numbers;DATEis a date;SINGLE_OPTIONS,RADIO,MULTIPLE_OPTIONSandCHECKBOXare a pick list (giveoptions). Any otherdataTypeanswers422. A field reads back with the nearest of these types andfieldKeycontact.<key>. - Set a field on a contact with
customFields: [{ "id": "…", "field_value": "…" }](orkeyinstead ofid,valueinstead offield_value). A list value is stored as one comma-separated text. - Opportunity fields.
model: "opportunity"answers400, and?model=opportunitylists none: Cook keeps custom fields on contacts. - Restricted fields you made private in Cook are left out of the list and
can’t be read, searched or set through this API (setting one answers
422). - Custom values are your workspace’s custom values, the same ones Cook’s
templates use.
fieldKeyis{{ custom_values.<key> }}, and that tag works in Cook’s emails, texts and routines. Renaming a value keeps its key.
Sending messages
POST /conversations/messages sends through Cook, from the contact’s
client’s own number or mailbox, under your approval rules.
type: "SMS"sends a text (message).type: "Email"sends an email (subject,htmlormessage).- Other types answer
400“<type>messages cannot be sent through this location: Cook sends SMS and Email through the API”. Log messages on other channels with/inboundinstead. - When Cook refuses, the answer is
400with Cook’s reason, for example “Not sent: this lead’s client has no live texting number of its own, and a client’s leads are never texted from another number.” attachments,scheduledTimestamp,templateId,emailCc,emailBcc, anemailToother than the contact’s email and atoNumberother than the contact’s phone answer422. Cook picks the sender and the thread, sofromNumber,emailFrom,threadIdandconversationProviderIdare not used.
Logging messages
POST /conversations/messages/inbound and /outbound write a message on the
conversation without sending anything.
- Conversations and channels. One GoHighLevel conversation holds a contact’s texts and calls together, and Cook files them in the conversation you name. Email (and Facebook and Instagram) have their own conversations; a message of that kind goes on the contact’s conversation for its channel, and the answer gives that conversation’s id.
- What an inbound message does in Cook: it updates the conversation and the lead’s timeline; “STOP” turns on do-not-disturb and “START” turns it off (no reply text is sent); an ordinary reply counts as the lead replying, so routines that start or stop on a reply do. Cook’s AI setter does not answer it, because the message didn’t come through Cook.
altIdmakes a repeat of the same message a no-op.
Workflows
Cook routines take the place of GoHighLevel workflows.
GET /workflows/lists your routines with UUID ids.POST /contacts/{contactId}/workflow/{workflowId}starts a run of that routine for the lead, with the checks Cook’s own Run button makes. A routine that is off answers400“This workflow is a draft. Cook enrolls contacts only in a published (switched on) workflow”. A routine for one client refuses a lead of another.eventStartTimeis accepted, but Cook’s appointment waits use the lead’s next meeting in Cook.DELETE /contacts/{contactId}/workflow/{workflowId}stops the lead’s runs of that routine. They read as canceled in the routine’s enrollment list.
Calendars
A GoHighLevel calendar is a Cook booking page; an appointment is a Cook meeting.
- Ids. Calendars, appointments, contacts and users have 20-character ids; pipeline stages and workflows have UUIDs. An object gets its id the first time Cook returns it, and keeps it. Objects you imported from GoHighLevel keep their GoHighLevel ids.
- Times are ISO 8601 with the offset of the
timezoneyou ask for, else the page’s. - Booking goes through the booking page, so the lead, the meeting, the calendar event and reminders are what a booking on the page makes.
Webhooks
None yet. GoHighLevel’s webhooks belong to a Marketplace app (an app
subscribes, rather than a URL you register through the API), and Cook does not
send Marketplace webhook events (INSTALL, ContactCreate, …) to apps. To
act when something changes in Cook (a new lead, a booking, a reply), use a
Cook routine that starts on that event.
Marketplace apps (OAuth)
An app built on GoHighLevel’s Marketplace OAuth (a setter bot, a dialer, a
reporting tool) moves to Cook by changing its OAuth and API addresses. Cook
serves GoHighLevel’s OAuth 2.0 authorization-code flow: the choose-location
page, POST /oauth/token, refresh, POST /oauth/locationToken and
GET /oauth/installedLocations, with GoHighLevel’s request and response
bodies.
1. Register the app in Cook
GoHighLevel registers apps in its Marketplace; Cook registers them through the API. Any Cook workspace’s owner or admin can do it, with their own full-access account key (Settings → API keys, made for a person, no narrowed areas):
POST https://api.trycook.ai/compat/ghl/oauth/clients
Authorization: Bearer ck_live_…
Version: 2021-07-28
Content-Type: application/json
{
"name": "Example Setter",
"redirectUris": ["https://setter.example.com/oauth/callback/highlevel"],
"scopes": ["contacts.readonly", "contacts.write", "conversations.write",
"conversations/message.write", "calendars/events.write",
"locations.readonly", "oauth.readonly", "oauth.write"]
}
The answer (201) carries clientId (GoHighLevel’s shape,
<appId>-<8 characters>), appId, clientSecret and installUrl, the
choose-location link to send people to. The client secret is shown once:
Cook keeps only its hash. Lost it? Delete the app and register it again.
redirectUris: up to 10,https(orhttponlocalhost/127.0.0.1), matched exactly.scopes: the GoHighLevel scopes the app may ask for. An unknown scope is refused (422).GET /oauth/clientslists your apps (no secrets) and how many workspaces installed each.DELETE /oauth/clients/{clientId}deletes an app and ends every token it holds, in every workspace, at once.
2. Swap the OAuth addresses
| GoHighLevel | Cook |
|---|---|
https://marketplace.gohighlevel.com/oauth/chooselocation | https://app.trycook.ai/compat/ghl/oauth/chooselocation (the installUrl host) |
https://services.leadconnectorhq.com/oauth/token | https://api.trycook.ai/compat/ghl/oauth/token |
https://services.leadconnectorhq.com/oauth/locationToken | https://api.trycook.ai/compat/ghl/oauth/locationToken |
https://services.leadconnectorhq.com/oauth/installedLocations | https://api.trycook.ai/compat/ghl/oauth/installedLocations |
https://services.leadconnectorhq.com (the API) | https://api.trycook.ai/compat/ghl |
The app sends people to the choose-location page with response_type=code,
client_id, redirect_uri and scope (and state, returned untouched).
The person signs in to Cook, picks the workspace (GoHighLevel’s location),
sees what the app would reach in plain words and installs or denies. Cook
then sends the browser to the redirect URI with ?code=… (or
?error=access_denied). Only an owner or admin of a paid workspace can
install an app.
The token calls are GoHighLevel’s, form-encoded (JSON is accepted too):
POST https://api.trycook.ai/compat/ghl/oauth/token
Content-Type: application/x-www-form-urlencoded
client_id=…&client_secret=…&grant_type=authorization_code&code=…&user_type=Location&redirect_uri=…
The answer is GoHighLevel’s token body: access_token, token_type
(Bearer), expires_in (86399), refresh_token, scope,
refreshTokenId, userType, locationId (or approvedLocations for
user_type=Company), companyId, userId and traceId. Call the API with
Authorization: Bearer <access_token> and the Version header, as on
GoHighLevel.
Tokens
- Codes are good once, for ten minutes, for the redirect URI they were issued for. A code presented again more than a minute after it was used ends what it was exchanged for.
- Access tokens last a day (
expires_in: 86399). Each one is a Cook key acting as the person who installed the app, in the workspace they picked, narrowed to what the approved scopes reach (below). Cook re-checks that person’s role and the workspace’s plan on every call. - Refresh tokens last a year and rotate:
grant_type=refresh_token&refresh_token=…returns a new pair, and the refresh token used is refused from then on (400“Invalid grant: refresh token is invalid”). The access token it replaced keeps working for five minutes. A used refresh token presented again after a minute ends the install, so store the new one before using it. - Company tokens (
user_type=Company) carryoauth.readonly/oauth.writeroutes:GET /oauth/installedLocationslists your one location;POST /oauth/locationToken(companyId,locationId) returns a location token.
Scopes
Cook’s permissions are coarser than GoHighLevel’s scopes, so a scope reaches the whole Cook area its calls use:
| GoHighLevel scope | Reaches in Cook |
|---|---|
contacts.readonly / .write | CRM and board, read / write (contacts, notes, tasks) |
conversations.*, conversations/message.*, opportunities.*, locations.readonly, locations/tags.*, locations/customFields.*, locations/customValues.*, calendars/events.* | CRM, read / write |
calendars.readonly / .write | Booking pages, read / write |
users.readonly / .write | Team, read / write |
workflows.readonly | Routines, read |
oauth.readonly / oauth.write | The OAuth routes above; no Cook data |
Any other documented GoHighLevel scope is accepted and reaches nothing in Cook. A token never gets full access, never registers apps and never decides approval cards. Adding a contact to a workflow needs a Cook key with routines write; no scope grants it.
Removing an app
- The workspace that installed it: revoke one of its keys (named
“OAuth ·
”) in Settings → API keys. The install ends: every token it holds stops and its refresh token is refused. - The app itself:
DELETE /marketplace/app/{appId}/installationswith its token. - The app’s maker:
DELETE /oauth/clients/{clientId}ends it everywhere.
Not served: Marketplace webhooks, grant_type=client_credentials, bulk
installs across several locations (a Cook company has one location), and the
Marketplace listing itself.
Gotchas
GET /contacts/lookupisn’t served. UseGET /contacts/search/duplicate.- Pipelines are read-only here. Make and change them in Cook.
- Calendar groups don’t exist in Cook:
groupIdfilters answer empty lists. DELETE /calendars/events/{eventId}cancels. The meeting stays in Cook and reads back ascancelled.- Tasks on a contact.
GET /contacts/{contactId}/taskslists the tasks made through this API. Tasks people add on Cook’s board are not linked to a lead, so they don’t show there. - One contact per email or phone. Creating a duplicate answers
400; usePOST /contacts/upsert. - Limits. Cook’s own limit applies: 300 calls a minute per key, then
429withRetry-Afterand GoHighLevel’sX-RateLimit-*headers. - Plan. A workspace without a paid plan gets
402.
What can’t move
- Workflow graphs. GoHighLevel has no API to read them. Rebuild them as Cook routines (Cook’s AI can do this from a description).
- Funnels, websites and snapshots. Not readable through GoHighLevel’s API; rebuild them in Cook.
- A2P 10DLC registration. It belongs to the carrier. Register your business again for texting in Cook.
- LC Phone numbers. They stay with GoHighLevel until you port them.
- The white-label mobile app stays with GoHighLevel.