CRM Custom API
Docs Home / API Reference / CRM Custom API

CRM Custom API

Any CRM, through Zapier, Make, n8n or your own endpoint

The CRM Integration connects Salesforce, HubSpot, Zoho, Pipedrive and Zendesk natively. The Custom API provider opens the same integration to everything else: you give it one HTTPS address, and every event the integration produces (a logged call, a note, a new contact, a request for your contact list) is delivered there as a signed JSON POST. Point that address at a Zapier, Make or n8n webhook and the workflow you build in their editor writes to whatever CRM, help desk or spreadsheet they connect to; point it at an API you already run and your own code is the CRM. Contacts flow the other way through a push endpoint on our side, so your CRM's contacts reach desk phones, the customer portal, caller ID and the AI agents exactly as a native provider's do.

Setup is on the CRM Integration page: choose Custom API as the provider, enter the endpoint URL and click Connect. The connected card shows the secret the platform uses on every request, the contact push address, and a Send Test Event button that delivers a sample event and shows exactly what your endpoint answered. This page is the contract your developers, or your workflow, build against.

How It Works

  1. Every operation is one POST to your endpoint URL with a JSON envelope. The event field says which operation it is, so one endpoint (or one workflow with a router step) serves them all.
  2. Each request carries the secret as a bearer token and a signature of the body, so your endpoint can reject anything that is not from Cloudspire.
  3. Your endpoint answers with JSON, or with nothing at all. Any 2xx status is an acknowledgement. If the reply carries an id (an activity id, a contact id) the platform records it; if it does not, the event still counts as delivered. A Zapier catch hook answers a fixed success before the Zap runs, and that is fine.
  4. A failure (a timeout, a 4xx or 5xx, no response, or a 2xx reply that itself says "success": false) is retried by the call logging queue, up to five times, a few minutes apart.
  5. Contacts come to Cloudspire through the push endpoint below. The Sync Now button on the card asks your endpoint for its contact list instead (the contacts.list event); use it when your endpoint is an API that can answer with data, not a catch hook.

Request Envelope

Every request is POST with Content-Type: application/json and this shape:

{
  "event": "call.logged",
  "sent_at": "2026-09-19T14:30:05-04:00",
  "data": { ... }
}
HeaderDescription
AuthorizationBearer {secret}: the secret shown on the connected CRM card. Compare it with the value you copied and refuse the request when it differs.
X-Cloudspire-EventThe same value as the body's event field, so a router can branch without parsing the body.
X-Cloudspire-SignatureLowercase hex HMAC-SHA256 of the raw request body, keyed with the secret. Verify it to prove the body was not altered in transit (see Verifying the Signature). The bearer token alone is enough for a workflow platform; verify the signature when your own code is the endpoint.

sent_at is ISO 8601 with the platform's timezone offset. There is no tenant field: the endpoint is configured per phone system, so everything that arrives at it belongs to that one account.

Events

call.logged

Sent a few minutes after every answered external call, inbound or outbound. Unlike the native providers, which log only calls that match a synced contact, the Custom API provider receives every answered external call and leaves the matching to you; when a synced contact did match, its id rides along.

{
  "event": "call.logged",
  "sent_at": "2026-09-19T14:30:05-04:00",
  "data": {
    "call_id": "sip1-atl.example-1789830000.1234",
    "direction": "inbound",
    "from_number": "+14045551234",
    "from_name": "Jane Smith",
    "to_number": "+14045550100",
    "started_at": "2026-09-19 14:26:11",
    "duration_seconds": 214,
    "disposition": "ANSWERED",
    "summary": "Summary: Caller asked about ...",
    "contact_id": "crm-12345"
  }
}
FieldDescription
call_idThe call's identifier, the same value that appears in your call records. Treat it as an opaque string and use it to deduplicate: a retried delivery carries the same id.
directioninbound or outbound. Internal extension-to-extension calls are never sent.
from_number / to_numberThe parties in E.164 where known. On an inbound call the caller and the number they dialed; on an outbound call the extension's caller ID and the number dialed.
from_nameThe caller's name as the phone system knew it: the caller ID name, or the matched contact's name when caller ID carried none. May be empty.
started_atWhen the call started, as YYYY-MM-DD HH:MM:SS in the phone system's timezone (whole seconds, no offset).
duration_secondsTalk time in seconds.
dispositionThe call outcome as recorded by the phone system, for example ANSWERED.
summaryThe AI call summary when call transcription with CRM notes is on for an extension that handled the call, else empty.
contact_idThe external_id of the synced contact whose phone number matched, or null when none matched.

Reply: optionally {"activity_id": "..."}; the id is shown on the card's call activity log. Any other 2xx reply is an acknowledgement.

contact.note

Sent when an AI agent saves a note to the customer's record with its CRM Note tool, and when the post-call analysis of an AI conversation is logged to the CRM. The note is free text; the contact id is the one the agent looked up or the one that matched the caller.

{
  "event": "contact.note",
  "sent_at": "2026-09-19T14:31:40-04:00",
  "data": {
    "contact_id": "crm-12345",
    "note": "Caller confirmed the appointment for Tuesday at 10."
  }
}

Reply: optionally {"activity_id": "..."}.

contact.create

Sent when an AI agent creates a record for a caller who had none (its CRM Create Contact tool). Fields the agent did not collect are empty strings.

{
  "event": "contact.create",
  "sent_at": "2026-09-19T14:32:02-04:00",
  "data": {
    "first_name": "Jane",
    "last_name": "Smith",
    "phone": "+14045551234",
    "email": "[email protected]",
    "company": "Acme"
  }
}

Reply: {"contact_id": "..."} when your system assigned one. With an id, the agent tells the caller the record was created and the contact is added to the synced list at once; without one, the agent says the record was sent, and the contact appears once your workflow pushes it back (below).

contacts.list

Sent only when an administrator clicks Sync Now on the card. Your endpoint may answer with a page of contacts; a catch hook cannot, and the card says so.

{
  "event": "contacts.list",
  "sent_at": "2026-09-19T14:35:00-04:00",
  "data": { "cursor": null, "limit": 100 }
}

Reply: {"contacts": [ ... ], "cursor": "next-page", "has_more": true}, each contact in the shape the push endpoint accepts. Return the same cursor you received, or omit it, to end the walk. An acknowledgement with no contacts array is read as zero contacts.

Pushing Contacts to Cloudspire

This is how your CRM's contacts reach desk phones, the customer portal, caller ID and the AI agents' CRM lookup. Your workflow calls it whenever a contact is created or changed in your CRM (a Zapier New or Updated Contact trigger, a Make or n8n schedule, or your own code), with the same secret as the bearer token.

POST https://api.cloudspirevoice.com/crm/contacts.php
Authorization: Bearer {secret}
Content-Type: application/json

{
  "contacts": [
    {
      "external_id": "crm-12345",
      "first_name": "Jane",
      "last_name": "Smith",
      "company": "Acme",
      "phone_work": "+14045551234",
      "phone_mobile": "+14045559876",
      "email": "[email protected]",
      "external_url": "https://crm.example.com/contacts/12345"
    }
  ]
}
FieldDescription
external_idRequired. Your CRM's stable id for the contact (up to 255 characters). It is the key: a second push with the same id updates the contact, and it is the contact_id you receive on call.logged when the contact's number matches.
first_name, last_nameUp to 100 characters each.
company, emailUp to 150 characters each.
phone_work, phone_mobileAny common format; numbers are normalized to E.164. A number that cannot be normalized is stored empty.
external_urlAn http or https link to the record in your CRM; it becomes the screen-pop link on the contact. Up to 500 characters.

Send the full record each time: a field left out is stored as empty, so your CRM remains the source of truth. Up to 500 contacts per request and 1 MB per body. The reply reports what happened:

{"ok": true, "inserted": 1, "updated": 0, "capped": 0, "rejected": 0, "total_contacts": 318, "contact_limit": 500}

capped counts new contacts not added because the account's contact limit was reached (existing contacts are always updated); rejected counts records with no external_id. A 401 means the secret is wrong or the Custom API CRM is not connected; a 403 means the CRM integration is no longer on the plan; a 409 means another push for this connection is still being stored, so retry after a few seconds (pushes for one connection are stored one at a time). Contacts pushed this way are removed when the CRM is disconnected, exactly like synced contacts.

Verifying the Signature

Compute HMAC-SHA256 over the raw request body (the exact bytes, before any JSON parsing) with the secret as the key, encode it as lowercase hex, and compare it with X-Cloudspire-Signature using a constant-time comparison.

// Node.js
const crypto = require('crypto');
const expected = crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-cloudspire-signature'] || ''));

# Python
import hmac, hashlib
expected = hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers.get('X-Cloudspire-Signature', ''))

Worked Example: Any CRM via Zapier

A Zap with a Webhooks by Zapier: Catch Hook trigger, a Paths step that branches on event, and your CRM's own actions on each path:

Endpoint URLThe catch hook URL Zapier shows when you add the trigger. Paste it into the Custom API provider's Endpoint URL box and click Connect.
Path: call.loggedYour CRM's Find Contact by from_number (or to_number on an outbound call), then Create Call or Create Activity with the duration, direction and summary.
Path: contact.noteYour CRM's Create Note on the contact whose id is contact_id.
Path: contact.createYour CRM's Create Contact from the name, phone, email and company.
A second ZapTrigger: your CRM's New or Updated Contact. Action: Webhooks by Zapier: Custom Request, POST to the contact push address with Authorization: Bearer {secret} and a body carrying the contact in the shape above. This is what puts your contacts on the phones and in front of the AI.

Make and n8n follow the same pattern with a Webhooks or Webhook trigger, a Router or Switch on event, and an HTTP module for the push. Because a catch hook acknowledges before the workflow runs, activity_id and contact_id replies are not available from Zapier; Make (a Webhook response module) and n8n (a Respond to Webhook node) can return them.

Click Send Test Event on the connected card at any point: it delivers a call.logged with sample data and "test": true, and the card shows the HTTP status and reply your endpoint gave, so a workflow can be built and tested before a real call ever arrives.