3CX integration — administrator & developer guide

Call Helm integrates with 3CX as an external CRM. Once connected, your 3CX phone system looks up callers in your Call Helm contacts (screen pop), searches your contacts from the 3CX client, creates contacts from 3CX, and journals completed calls back into Call Helm.

What works today, in one line: contact lookup / screen pop, contact search, contact creation, and call journaling are all functional. Journaling and CTI (lookup/search/create) sit behind different plan tiers, and journaling additionally requires a seated, mapped agent and a matching contact — see Entitlements and gating below.

This is the in-depth companion to the Help Center's 3CX page (which has the short setup steps). It covers the exact endpoints, payloads, authentication, and troubleshooting for administrators configuring the integration.


How it works

Call Helm publishes a 3CX CRM template (an XML file) that tells 3CX which HTTP requests to make and how to read the responses. 3CX is the caller; Call Helm is the server. There is no reverse connection from Call Helm into 3CX.

3CX event Request 3CX makes Status What Call Helm does
Call rings / is placed GET /api/3cx/webhooks/lookup?number=… Works (Business/Enterprise) Finds contacts whose normalized phone number matches; returns name, company, email and a contactUrl 3CX opens for screen pop (Call Helm's Active Call panel for that number).
Agent searches the CRM GET /api/3cx/contacts/search?query=… Works (Business/Enterprise) Free-text search over first/last name, email, company and phone.
Agent creates a contact in 3CX POST /api/3cx/contacts/create Works (Business/Enterprise) Creates the contact in your organization.
Call ends POST /api/3cx/webhooks/call Works (Professional and up) Records the call in Call Helm with direction, duration, status and agent attribution — subject to the seat gate and contact match described below.
Template download (setup only) GET /api/3cx/template?apiKey=… Works Returns the XML template pre-filled with your secret and URL, scoped to what your plan includes.

Every device request is authenticated with your organization's 3CX webhook secret, sent as Authorization: Bearer <secret> (see Authentication). Lookups, searches, creates and journals are logged to a per-organization event table and summarized as counters on the 3CX settings page.


Entitlements and gating

The integration ships in two tiers, and the template you download reflects your plan automatically:

Capability Minimum plan Notes
Call journaling Professional Always included in the downloaded template — Call Helm doesn't ask you to unlock it separately.
Screen pop, contact search, contact creation ("CTI") Business / Enterprise The downloaded template only includes these scenarios when your organization is entitled. A Professional org gets a journaling-only template; Business/Enterprise gets journaling plus the CTI scenarios.

If you're on Professional and want screen pop, upgrade the organization, then re-download the template — the new download will include the CTI scenarios.

Two further conditions gate whether a given call actually gets journaled:

  1. Seat gate. A call is only journaled if the handling 3CX extension is mapped (see Step 4) to an active, seated Call Helm agent. An unmapped extension, or one mapped to an agent who has lost their seat, is silently skipped — no error is returned to 3CX, and nothing is written. Each extension you want journaled needs its own agent seat and its own mapping.
  2. Contact match. Journaling looks the external number up against your contacts the same way screen pop does. If the number doesn't match an existing contact, 3CX still gets a normal 200 response, but no call record is written. In practice this means journaling records calls to numbers you already track as contacts (for example, the numbers on your call lists) — a call to a number that isn't a contact is expected to go unrecorded, not a bug to report.

Prerequisites

  • Call Helm: an account with the Organization Admin (or Super Admin) role. Generating the webhook secret and managing agent mappings require admin; viewing status does not.
  • 3CX: admin access to the 3CX Management Console (the CRM Integration settings), on a 3CX version that supports REST-type CRM templates (the template is Version="1" of the 3CX CRM XML format).
  • Network: your 3CX server must be able to reach your Call Helm URL over HTTPS. Call Helm makes no calls back into 3CX, so no inbound firewall rules are required on the 3CX side.

Setup

Step 1 — Generate the webhook secret (Call Helm)

  1. Sign in as an Organization Admin and open Settings → Integrations.
  2. On the 3CX card, click Configure.
  3. Click Generate Webhook Secret. The secret (3cx_ followed by 64 hex characters) is displayed with a Copy button.

Copy it now. The secret is only shown in the response to generation. Reloading the page shows the integration as Active / configured but does not display it again (the status endpoint deliberately omits it). If you lose it, click Regenerate Secret — this issues a new secret and invalidates the old one immediately, so you must update 3CX (or re-download the template) afterwards.

Generating a secret also enables the integration and records your Call Helm URL as the CRM URL.

Step 2 — Download the XML template (Call Helm)

Click Download callhelm.xml. The browser saves the 3CX CRM template with your webhook secret and Call Helm URL already filled in as parameter defaults. Which scenarios the file contains depends on your plan — see Entitlements and gating.

The file is generated by GET /api/3cx/template?apiKey=<your secret> — the same URL is returned as downloadUrl by the generation call, so you can also fetch it with curl:

curl -o callhelm.xml "https://<your-call-helm-host>/api/3cx/template?apiKey=3cx_…"

(The server names the attachment call-helm-3cx.xml; the in-app button saves it as callhelm.xml. Either name is fine — 3CX reads the content.)

Step 3 — Install the template in 3CX

  1. Open the 3CX Management Console → Settings → CRM Integration.

  2. Click + Add CRM Template and upload the XML file.

  3. Select CallHelm in the CRM drop-down (the template's Name is CallHelm).

  4. Review the parameters the template defines:

    Parameter (3CX label) Default Notes
    Call-Helm API Key (WebhookSecret) your secret Sent as Authorization: Bearer <secret> on every request.
    Call-Helm URL (BaseUrl) your Call Helm URL Must be the public HTTPS origin of your Call Helm instance, no trailing slash.
    Enable Call Journaling (ReportCallEnabled) True Controls whether 3CX posts to the journaling endpoint. This default only takes effect the first time you configure the template — once you've saved a configuration, 3CX keeps whatever you last saved. See the troubleshooting gotcha below if journaling stops working after a template update.
    Call Subject (CallSubject) "Call-Helm Call Log" Defined by the template; Call Helm does not currently read it.
    Allow contact creation from 3CX (CreateContactEnabled) True Only present when your plan includes CTI. Controls whether 3CX offers "create contact" against Call Helm.
  5. Tick Enable Call Journaling and click Save, even though it defaults to on — see the troubleshooting section for why this matters.

  6. Click Test. 3CX runs a scenario against Call Helm; a 200 response means the secret and URL are correct.

  7. Click Save again.

Template facts worth knowing: numbers are passed as-is (<Number Prefix="AsIs" MaxLength="15" />, no prefix stripping); up to 16 concurrent requests are allowed (MaxConcurrentRequests="16"); the template declares no 3CX-side authentication (<Authentication Type="No" />) because auth is the header.

Running 3CX with multiple companies? CRM integration in 3CX is configured per company. Repeat steps 1–7 (or at least steps 2–7 with a template downloaded for the right organization) in every company whose calls you want journaled or screen-popped — a company without the template installed and enabled won't send any requests to Call Helm, even if another company on the same PBX is fully working.

Step 4 — Map extensions to agents (optional)

Agent mappings attribute journaled calls to a Call Helm agent by the 3CX extension that handled them, and they are what satisfies the seat gate described in Entitlements and gating — a call from an unmapped extension, or one mapped to an agent without an active seat, is not journaled. In Settings → Integrations → 3CX → Show Agent Mappings, add one row per agent:

  • Extension (required) — e.g. 101
  • Agent Email (required by the form) — resolved to the matching active Call Helm agent in your organization
  • First / Last name (optional)

Mappings are keyed on (organization, extension); adding the same extension again updates the existing row. Delete a mapping with the trash icon.


Monitoring

Once configured, the 3CX settings page shows counters for the last 30 days: Total Events, Contact Lookups, Calls Journaled, Contacts Created (searches are counted in the total but not shown separately). They come from GET /api/3cx/stats and summarize lookups, searches, journals and contact creations over the period — these increment as calls and requests come in.


Authentication

3CX device requests authenticate with your organization's webhook secret:

  • Current templates send Authorization: Bearer <secret> — this is what a freshly downloaded or regenerated template uses, and what this guide documents throughout.
  • Legacy x-api-key: <secret> is still accepted on the same endpoints, so an older installed template keeps working without re-importing anything. New templates use Bearer; there's no need to switch an already-working legacy install unless you're re-downloading the template anyway.

Notes on the secret itself:

  • Format: 3cx_ + 64 lowercase hex characters (32 random bytes). Requests whose secret does not start with 3cx_ are rejected before any database lookup.
  • Scope: authenticates only the device endpoints listed below. It cannot be used for any other Call Helm route, and it is not a general API credential.
  • Validation: the secret must match an integration row that is enabled. Disabling the integration or regenerating the secret causes 401 Invalid API key on all device endpoints.
  • Rotation: Regenerate Secret in Settings replaces the secret atomically (one integration row per organization). Update 3CX right after — either re-enter the parameter or download a fresh template.
  • Storage & transport: treat the secret like a password. Always use HTTPS for the Call Helm URL, and rotate it immediately if it is ever exposed (e.g. pasted into a ticket or chat).

API reference

Base URL: your Call Helm origin (the BaseUrl parameter). All responses are JSON unless noted.

Device endpoints (called by 3CX; auth = Authorization: Bearer <secret>, or legacy x-api-key)

Common error responses:

Status Body Cause
401 { "error": "Invalid API key", … } Secret missing, unknown, malformed, or integration disabled.
403 { "error": "…not available on your current plan", "upgradeRequired": true, "contacts": [] } Lookup called by an organization without CTI entitlement.
500 { "error": "Database error" … } / "Internal server error" Server-side failure; details are in Call Helm's logs.

GET /api/3cx/webhooks/lookup — works (Business/Enterprise)

Find contacts by phone number (used for screen pop). The legacy GET /api/3cx/contacts/lookup is the same handler at an older URL, kept working for templates that haven't been re-downloaded.

Query param Required Description
number yes The phone number as 3CX has it. Matched against your contacts using common phone-number variants (with/without country code, punctuation stripped), so +1 (555) 123-4567 and 15551234567 match the same contact.

Up to 10 contacts are returned.

curl -H "Authorization: Bearer 3cx_…" \
  "https://<host>/api/3cx/webhooks/lookup?number=%2B15551234567"

Response 200:

{
  "success": true,
  "contacts": [
    {
      "id": "8f3c…",
      "firstName": "Ada",
      "lastName": "Lovelace",
      "company": "Analytical Engines",
      "email": "ada@example.com",
      "phoneBusiness": "+15551234567",
      "phoneMobile": "+15551234567",
      "phoneHome": "",
      "contactUrl": "https://<host>/dashboard/active-call/%2B15551234567"
    }
  ]
}

No match returns 200 with "contacts": [] (3CX then treats the caller as unknown). 400 { "error": "Missing phone number", "contacts": [] } if number is absent. 403 if your organization doesn't have screen-pop entitlement. contactUrl opens Call Helm's Active Call panel for that number, so the agent can take notes during the call.

GET /api/3cx/contacts/search — works (Business/Enterprise)

Free-text contact search (the 3CX "Search CRM" action).

Query param Required Description
query yes, ≥ 2 characters Case-insensitive substring match against first name, last name, email, company, and phone number.

Up to 20 results. 400 { "error": "Query too short", "contacts": [] } for a missing or 1-character query.

curl -H "Authorization: Bearer 3cx_…" "https://<host>/api/3cx/contacts/search?query=lovelace"

Response shape is the same as lookup, minus phoneHome, and contactUrl points at the contact record: https://<host>/dashboard/contacts/<id>.

POST /api/3cx/contacts/create — works (Business/Enterprise)

Creates a contact in your organization from 3CX's CreateContactRecordFromClient scenario.

Field Required Notes
FirstName one of these three
LastName one of these three
Company one of these three
Number no The contact's phone number.
Email no

400 { "error": "At least one of FirstName, LastName, or Company is required" } if none of the three name fields is present.

Response 200:

{
  "success": true,
  "id": "8f3c…",
  "firstName": "Ada",
  "lastName": "Lovelace",
  "email": "ada@example.com",
  "company": "Analytical Engines",
  "phoneMobile": "+15551234567",
  "contactUrl": "https://<host>/dashboard/contacts/8f3c…"
}

POST /api/3cx/webhooks/call — works (Professional and up)

3CX posts this from the ReportCall scenario when Enable Call Journaling is on. The legacy POST /api/3cx/calls/journal is the same handler at an older URL. Subject to the seat gate and contact match — a request that doesn't clear those still gets a 200, just with nothing recorded.

Field Type / values Use
CallType Inbound, Outbound, Missed, Notanswered Missed/Notanswered → status no-answer; anything else → completed.
Number string The external party's number; used to match the contact.
CallDirection Inbound / Outbound Call direction and which side is the external number.
Name string Name 3CX matched.
EntityId string Contact ID from a prior lookup; if present, skips the contact search.
Agent string Extension number → agent mapping for attribution and the seat gate.
AgentEmail, AgentFirstName, AgentLastName string Recorded in the call notes.
Duration hh:mm:ss or mm:ss Converted to seconds; a call with 0 duration is recorded as not answered.
CallStartTimeUTC, CallEndTimeUTC ISO 8601, e.g. 2026-09-06T14:03:00Z Start/end timestamps (start defaults to now if omitted).
QueueExtension string Recorded in notes.

Response 200, whether or not anything was journaled:

{ "journaled": true, "matched": true, "marked": 1 }

journaled: false with skipped: true, reason: "extension_not_seated" means the extension wasn't mapped to a seated agent. matched: false means the number didn't match a contact — 3CX's request still succeeded, but there was nothing to attach the call to.

GET /api/3cx/template — works

Returns the CRM XML template (Content-Type: application/xml, attachment call-helm-3cx.xml), scoped to your plan's entitlements.

Query param Required Description
apiKey yes Your 3cx_ secret. 400 Missing apiKey parameter / 401 Invalid API key otherwise.

This is the one device endpoint that takes the secret as a query parameter (so the settings page can offer it as a download link). Avoid pasting that URL into chat or tickets — it contains your secret.

Dashboard endpoints (called by the Call Helm settings page; auth = signed-in session)

These power the Settings screen. They use the signed-in user's session cookie, resolve the organization from that user's active membership (a client-supplied organizationId is ignored), and are not usable with the 3cx_ secret. Listed for completeness; the UI is the supported way to use them.

Endpoint Role Purpose Response
GET /api/3cx/setup any active member Integration status { enabled, configured, crmUrl, threeCxServerUrl, settings } — never includes the secret; { enabled:false, configured:false } if not set up.
POST /api/3cx/setup admin Generate / regenerate the webhook secret { success, apiKey, crmUrl, downloadUrl }
GET /api/3cx/agents any active member List extension mappings { mappings: [ … ] } — one entry per extension→agent mapping (extension, agent, email, name).
POST /api/3cx/agents admin Upsert a mapping. Body: { extension (required), agentEmail, agentFirstName, agentLastName, agentId? } — an email is resolved to a seated member in your organization; agentId, if given, must also be a member of your organization (400 Invalid agent for this organization otherwise). { success, mapping }
DELETE /api/3cx/agents/{id} admin Remove a mapping (scoped to your organization) { success: true }
GET /api/3cx/stats?days=30 any active member Event counters for the last days days (default 30) { total_events, lookups, journals, contacts_created, searches }

Errors: 401 Unauthorized (no session), 403 No active organization membership, 403 Admin access required.


Troubleshooting

"Test" fails in 3CX

  • The Call-Helm URL must be the public HTTPS origin your 3CX server can reach (no trailing slash, no path). Try a lookup or template URL from the 3CX server with curl and your secret.
  • 401 → the secret is wrong, was regenerated, or the integration is disabled. Regenerate and re-enter it, or re-download the template.
  • Development instances behind tunnels (e.g. ngrok) may require the tunnel to be running; the in-app template download adds a header to bypass the tunnel's browser-warning interstitial for that case, but 3CX itself doesn't need it once the tunnel is up.

Screen pop shows "unknown caller" for a known contact

  • Lookup matches using common phone-number variants (digits-only, with/without a leading country code), so most formatting differences are already handled. If it still doesn't match, check the number is actually stored on that contact and store numbers in the format your PBX sends (E.164 recommended) if you continue to see misses.
  • Confirm your plan includes screen pop — see Entitlements and gating. A 403 from the lookup endpoint means it doesn't.

Calls aren't showing up in Call Helm (journaling)

Work through these in order:

  1. Enable Call Journaling must be ticked and Saved. The template defaults it on, but only the first time you configure it — a previously saved configuration is what 3CX actually uses. Open Settings → CRM Integration, confirm it's ticked, click Save.
  2. Restart the 3CX service after changing or re-uploading the template. 3CX loads the CRM template into memory at service start and does not re-read it live. A corrected template will keep appearing to "not work" until you restart the 3CX service (or reboot the PBX). This is the most common cause of a journaling setup that looks right but produces nothing.
  3. Multi-company 3CX: the template has to be installed and enabled in every company — it's a per-company configuration. A call in a company without it installed won't journal even if another company is working.
  4. The call must be answered and completed. Journaling fires on call end for connected calls; an unanswered ring has nothing to report.
  5. The extension has to be mapped to a seated agent. See Step 4. Check the response body — skipped: true, reason: "extension_not_seated" confirms this is the cause.
  6. The number has to match an existing contact. A journaled: true, matched: false response means 3CX's request succeeded but no contact was found for that number — add the number as a contact if you want future calls to it recorded.

3CX shows an error creating a contact

  • Confirm your plan includes CTI (Business/Enterprise) — see Entitlements and gating.
  • Check that at least one of First Name, Last Name, or Company is being sent — the endpoint rejects a request with none of the three.

Known issues and limitations

  1. The webhook secret is shown once at generation and cannot be re-displayed — copy it then, and rotate it if it leaks.
  2. Journaling requires a matching contact. A call to a number that isn't already a Call Helm contact is not recorded — this is how matching works, not a bug.
  3. Journaling requires a seated, mapped extension. An unmapped extension, or one mapped to an agent without an active seat, is silently skipped.
  4. No click-to-dial or presence. The template has no dial-out or reverse scenarios, and the optional 3CX server URL setting is stored but unused.
  5. One integration per organization (one secret, one template) and one 3CX extension per mapping row.
  6. settings flags are informational. call_journaling_enabled / contact_creation_enabled / auto_create_contacts are saved on secret generation but not enforced server-side; the 3CX template parameters (and your plan's entitlement) are the real switches.
  7. In-app "View 3CX Integration Guide" link opens this page (/help/integrations/3cx).

Still need help?

Our support team responds within 24–48 hours.

Email support