Integrations

Use Calldesk with HighLevel

Two HighLevel workflows connect the platforms: one that places a Calldesk AI call for a contact, and one that receives the result and updates the contact. It uses only HighLevel's built-in Custom Webhook action and Inbound Webhook trigger. There is nothing to install, and no marketplace app is involved. See also the API reference.

Not an official HighLevel product; HighLevel is a trademark of its owner. Both HighLevel features used here are LC Premium workflow items, so execution costs apply on your HighLevel side (see Costs).

Prerequisites

  • A Calldesk workspace with a published agent and a phone number whose outbound agent is set (Numbers, then the number, then Outbound Call Agent).
  • A Calldesk API key: Settings, API Keys, create a key. Keys start with cdk_live_ and are pinned to one workspace. Copy it when it is shown.
  • The id of the phone number to call from. Call GET https://calldesk.tech/api/v1/me to get your workspace id, then GET https://calldesk.tech/api/v1/tenants/<workspace id>/phone-numbers and use the id of the number (not the number itself).
  • A HighLevel sub-account with workflows, and LC Premium triggers and actions enabled for it (HighLevel agency settings).
  • To use Calldesk variables in the agent, put {{first_name}}-style placeholders in the agent prompt or spoken lines.

Workflow A: place a Calldesk call

Use any HighLevel trigger that has a contact with a phone number (for example Contact Tagged, Form Submitted, Appointment Status). Then add one action:

  1. Add the action Custom Webhook.
  2. Method: POST. URL: https://calldesk.tech/api/v1/phone-numbers/<YOUR_CALLDESK_PHONE_NUMBER_ID>/call.
  3. Under headers, add Authorization = Bearer <YOUR_CALLDESK_API_KEY> and Content-Type = application/json.
  4. Choose the CUSTOM event option so the action sends a raw JSON body, and paste the body below. The {{contact.*}} merge fields are filled in by HighLevel when the workflow runs.
  5. Save and publish the workflow, then test it with a contact that has your own mobile number.
Request body
{
  "toNumber": "{{contact.phone}}",
  "variables": {
    "first_name": "{{contact.first_name}}",
    "last_name": "{{contact.last_name}}",
    "email": "{{contact.email}}",
    "reason": "Following up on your enquiry"
  }
}

toNumber must be E.164 (+14155550123); make sure the contact's phone is stored that way. variables is optional: each key becomes a placeholder for this call only (letters, digits and underscores in the name; string values up to 500 characters; up to 25 keys). Empty values are ignored, so a contact with no last name does not break the call. A successful request returns 201 with { "call": { "sid": "...", "to": "..." } }; the call is then dialled by Calldesk.

Workflow B: call completed

  1. In HighLevel create a new workflow with the trigger Inbound Webhook. Copy the webhook URL it shows. It accepts a JSON POST.
  2. In Calldesk open Integrations, Webhooks, Add endpoint. Paste the HighLevel URL, tick Call completed, tick Flat payload, and add it. (Registering webhooks needs a signed-in owner or admin; API keys cannot create webhooks. The API field is format: "flat".)
  3. Use the endpoint's Test button, or place one real call, so HighLevel receives sample data. In the trigger click Fetch sample requests (the sample list can sit below the button, so scroll), select the received request as the Mapping Reference and save the trigger. HighLevel will not let the workflow save until a mapping reference is chosen.
  4. Add the action Create/Update Contact and map phone to Phone, email to Email and name to First name (use Add field for First name). Without the name mapping, new contacts are created with the phone number only. HighLevel needs an email or phone in the payload to create or match a contact; phone is always present on call events.
  5. Add Add Note (or Send Internal Notification) using the received summary, outcome, duration_seconds and transcript_url. Insert them with the tag icon in the note editor so they appear as field pills; text typed by hand is saved literally.
  6. Add an If/Else on outcome (values such as answered, voicemail, transferred, no_answer) and Add Tag per branch; custom variables arrive as var_<name> and can feed custom fields.

Calldesk signs every delivery (X-CallDesk-Signature), but HighLevel does not verify signatures. The secret Inbound Webhook URL is the only protection, so treat it like a password. If it leaks, delete the trigger in HighLevel, create a new one and update the Calldesk endpoint.

The flat payload

With format: "flat" a delivery is one single-level JSON object. Keys are fixed snake_case with no spaces. name and email are present only when known (from call variables or extracted call data), so HighLevel never sees empty identity fields. transcript_url opens the call in your Calldesk dashboard and requires signing in to your workspace; transcripts are never sent in the payload and the link is not public. It may be empty for a call whose log row is not known yet.

call.completed, flat
{
  "event": "call.completed",
  "created_at": "2026-10-09T18:01:00.000Z",
  "call_id": "CA0123456789abcdef",
  "phone": "+14155550123",
  "name": "Dana Lee",
  "email": "dana@example.com",
  "summary": "Dana asked to move her appointment to Friday at 3pm. Confirmed.",
  "outcome": "answered",
  "duration_seconds": 42,
  "direction": "outbound",
  "transcript_url": "https://calldesk.tech/dashboard/calls/<call-log-id>",
  "started_at": "2026-10-09T18:00:00.000Z",
  "ended_at": "2026-10-09T18:00:42.000Z",
  "agent_name": "Reminder agent",
  "var_first_name": "Dana",
  "var_reason": "Following up on your enquiry"
}

phone is always the other party: the person called on outbound calls, the caller on inbound calls. Without the flat option the payload keeps its nested { event, created_at, data } shape.

Machine-readable configuration

The same two workflows as data, for scripting or for building a HighLevel snapshot. Placeholders in angle brackets must be replaced; never commit real keys.

Workflow A (JSON)
{
  "name": "Place a Calldesk call",
  "trigger": {
    "type": "any HighLevel trigger that has a contact with a phone, e.g. Contact Tagged \"call-me\" or Form Submitted"
  },
  "actions": [
    {
      "type": "Custom Webhook",
      "method": "POST",
      "url": "https://calldesk.tech/api/v1/phone-numbers/<YOUR_CALLDESK_PHONE_NUMBER_ID>/call",
      "headers": [
        {
          "key": "Authorization",
          "value": "Bearer <YOUR_CALLDESK_API_KEY>"
        },
        {
          "key": "Content-Type",
          "value": "application/json"
        }
      ],
      "body_mode": "raw JSON (event CUSTOM)",
      "body": {
        "toNumber": "{{contact.phone}}",
        "variables": {
          "first_name": "{{contact.first_name}}",
          "last_name": "{{contact.last_name}}",
          "email": "{{contact.email}}",
          "reason": "Following up on your enquiry"
        }
      }
    }
  ]
}
Workflow B (JSON)
{
  "name": "Calldesk call completed",
  "trigger": {
    "type": "Inbound Webhook",
    "method": "POST",
    "url": "<URL_HIGHLEVEL_SHOWS_YOU_ON_THE_TRIGGER>"
  },
  "calldesk_registration": {
    "where": "Calldesk dashboard, Integrations, Webhooks",
    "url": "<URL_HIGHLEVEL_SHOWS_YOU_ON_THE_TRIGGER>",
    "events": [
      "call.completed"
    ],
    "format": "flat"
  },
  "actions": [
    {
      "type": "Create/Update Contact",
      "map": {
        "phone": "phone",
        "email": "email",
        "name": "name"
      }
    },
    {
      "type": "Add Note",
      "body": "Calldesk call ({{inboundWebhookRequest.direction}}, {{inboundWebhookRequest.duration_seconds}}s): {{inboundWebhookRequest.summary}} Transcript: {{inboundWebhookRequest.transcript_url}}"
    },
    {
      "type": "If/Else",
      "condition": "outcome equals transferred",
      "then": [
        {
          "type": "Add Tag",
          "tag": "calldesk-transferred"
        }
      ]
    },
    {
      "type": "Add Tag",
      "tag": "calldesk-called"
    }
  ],
  "note": "Verified in a HighLevel sub-account: the received fields are available as inboundWebhookRequest.<key> (direction, duration_seconds, summary, transcript_url, ...). Choose them from the field picker after sending a sample event; they appear as pills such as \"Inbound Webhook Trigger . Summary\"."
}
Workflow A (YAML)
name: "Place a Calldesk call"
trigger:
  type: "any HighLevel trigger that has a contact with a phone, e.g. Contact Tagged \"call-me\" or Form Submitted"
actions:
  - type: "Custom Webhook"
    method: "POST"
    url: "https://calldesk.tech/api/v1/phone-numbers/<YOUR_CALLDESK_PHONE_NUMBER_ID>/call"
    headers:
      - key: "Authorization"
        value: "Bearer <YOUR_CALLDESK_API_KEY>"
      - key: "Content-Type"
        value: "application/json"
    body_mode: "raw JSON (event CUSTOM)"
    body:
      toNumber: "{{contact.phone}}"
      variables:
        first_name: "{{contact.first_name}}"
        last_name: "{{contact.last_name}}"
        email: "{{contact.email}}"
        reason: "Following up on your enquiry"
Workflow B (YAML)
name: "Calldesk call completed"
trigger:
  type: "Inbound Webhook"
  method: "POST"
  url: "<URL_HIGHLEVEL_SHOWS_YOU_ON_THE_TRIGGER>"
calldesk_registration:
  where: "Calldesk dashboard, Integrations, Webhooks"
  url: "<URL_HIGHLEVEL_SHOWS_YOU_ON_THE_TRIGGER>"
  events:
    - "call.completed"
  format: "flat"
actions:
  - type: "Create/Update Contact"
    map:
      phone: "phone"
      email: "email"
      name: "name"
  - type: "Add Note"
    body: "Calldesk call ({{inboundWebhookRequest.direction}}, {{inboundWebhookRequest.duration_seconds}}s): {{inboundWebhookRequest.summary}} Transcript: {{inboundWebhookRequest.transcript_url}}"
  - type: "If/Else"
    condition: "outcome equals transferred"
    then:
      - type: "Add Tag"
        tag: "calldesk-transferred"
  - type: "Add Tag"
    tag: "calldesk-called"
note: "Verified in a HighLevel sub-account: the received fields are available as inboundWebhookRequest.<key> (direction, duration_seconds, summary, transcript_url, ...). Choose them from the field picker after sending a sample event; they appear as pills such as \"Inbound Webhook Trigger . Summary\"."

Errors and limits

  • 400 bad toNumber or invalid variables (the message names the field). Nothing is dialled.
  • 401 missing, revoked or wrong API key. 404 unknown phone number id or a number in another workspace. 403 your workspace is not allowed to place calls (for example a pilot limit).
  • 429 too many calls too fast. Calls are paced per workspace (currently a short burst, then about one call every two seconds). In HighLevel, add a Wait step before the webhook, or use the action's retry behaviour, when many contacts enter at once. For large lists use Calldesk batch calls instead.
  • 502 the voice provider rejected the call; the error text carries the reason, for example an invalid or unreachable destination number.
  • Webhook deliveries to HighLevel time out after 8 seconds and are not retried. A failed delivery is only logged on the Calldesk side; use the Test button to check the endpoint.
  • Outbound calls cost money and respect your plan, calling hours rules and local regulations. You are responsible for consent to contact each person.

Security and costs

  • Never paste a real cdk_live_ key into a HighLevel snapshot or any template you share: headers are copied with a snapshot. Put the key in each sub-account after import, and use a dedicated key per client so you can revoke it (Settings, API Keys).
  • An API key can place calls and read your workspace data. Keep it out of screenshots, forum posts and shared workflows.
  • The Inbound Webhook URL is a secret (see above). Rotate it if shared.
  • Custom Webhook and Inbound Webhook are LC Premium items in HighLevel. HighLevel states new sub-accounts get 100 free executions once premium items are enabled, then executions are billed by HighLevel to the account or, with rebilling on, the sub-account. Check HighLevel's current price. Calldesk bills calls separately, per your plan.

Troubleshooting

  • Nothing happens in Workflow A. Open the contact's workflow history in HighLevel and the Custom Webhook step's response. A 401 means the Authorization header is wrong (it must be Bearer followed by the key).
  • 400 about toNumber. The contact's phone is empty or not in E.164. Add a country code, or use a formatter step first.
  • The agent says "{{first_name}}" or ignores the name. The placeholder name in the agent must match the variables key exactly, and the merge field must not be empty.
  • Workflow B never triggers. The Calldesk endpoint must be subscribed to call.completed; the workflow must be published; the call must have ended. Use the Test button in Calldesk. Calls on some engines may need extra time for analysis before summary is filled.
  • Payload looks nested. The endpoint was created without Flat payload. Delete it and add it again with Flat payload ticked, or PATCH its format to flat using a signed-in session.
  • No contact created. HighLevel needs an email or phone in the received data. Re-select the mapping reference after the payload shape changed (HighLevel asks you to re-pick it).
  • Test succeeds but real calls are missing. The flat payload for calls placed on the CallDesk voice engine requires the matching engine release; if phone or transcript_url is empty, contact support.

Building a HighLevel snapshot

A snapshot must be created inside a HighLevel agency account; this page cannot supply one. To build one: create the two workflows above in a template sub-account, leave the Authorization header as the literal text Bearer <YOUR_CALLDESK_API_KEY> and the number id as <YOUR_CALLDESK_PHONE_NUMBER_ID>, replace the Inbound Webhook URL step with a note (the URL is created per sub-account), then in Agency settings choose Snapshots, Create new, select the workflows and share the link. After importing, each client replaces the two placeholders and registers their own Inbound Webhook URL in Calldesk.