Skip to main content

API Reference v1

REST + Webhooks integration reference (version 2026-06-15)

CRM Integration Quickstart

The fastest path to connect a CRM to Heilo. The full API reference is below.

Integrate webhook-first: the call.completed event is the primary data source (it carries the recording link and the processed transcript). REST /calls is a supplement — introspection and reading selected metadata.

  1. Expose a webhook endpoint in your CRM or middleware (no code: use Zapier/Make — see the guide below).
  2. Add a subscription in Heilo (Settings → Integrations) for call.completed (optionally also call.outbound.attempted).
  3. Receive call.completed and verify the signature (Heilo-Signature header, HMAC — see below).
  4. Deduplicate by event_id (heilo-event-id header); data.call_id groups events of the same call.
  5. Map the fields to your CRM: find/create a contact by phone, create a lead/deal, and attach an activity/note (summary + recording link).

No code? The no-code connection guide (Zapier/Make) walks through it step by step.

Authentication

Public API uses Bearer tokens. Generate an API key from the "API keys" card on the Integrations page and send it in the header:

Authorization: Bearer hk_live_AbC1MnPq...

Heilo has three auth modes:

  • Bearer (API keys hk_live_…) — for Public API. No CSRF, no cookies.
  • Session cookies — for the web app (heilo.io). Do NOT use for Public API.
  • HMAC-SHA256 — for Webhooks Heilo sends to YOUR endpoint (you verify signature header).
ScopeMeaning
read.callsRead calls: GET /api/v1/calls, GET /api/v1/calls/:id
write.calls, read.contacts, write.contacts, manage.webhooks, manage.api_keysReserved for planned API endpoints — don't select them ahead of time.

The environment field in the /me response is always live today. Test keys are planned.

Base URL

All public endpoints live under /api/v1/. Production:

https://www.heilo.io/api/v1

Endpoints

Every /api/v1 endpoint requires Bearer. /me below is for key introspection; reading calls is in the "Calls" section. When integrating a CRM, treat webhooks as the primary data source — REST is for introspection and reading selected metadata.

GET/api/v1/me

API key introspection — returns the key id, user_id, scopes, rate limit. Useful for Zapier/Make "connection test".

Request

curl https://www.heilo.io/api/v1/me \
  -H "Authorization: Bearer hk_live_AbC1MnPq..."

Response

{
  "success": true,
  "data": {
    "api_key_id": "a1b2c3d4-5e6f-7081-92a3-b4c5d6e7f809",
    "user_id": "5f4e3d2c-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
    "organization_id": "7a8b9c0d-1e2f-4a3b-9c8d-7e6f5a4b3c2d",
    "scopes": ["read.calls", "manage.webhooks"],
    "rate_limit_per_hour": 1000,
    "environment": "live"
  },
  "meta": { "timestamp": "2026-06-03T12:34:56Z" }
}

Use case: Zapier connection test during custom integration setup. A 200 response proves the key + network work.

Calls

Read endpoints for calls. Require the read.calls scope.

GET/api/v1/calls

List the organization's calls. Pagination (page/limit≤100), filters: direction, status, date range (dateFrom/dateTo). Returns has_more.

Query parameters

ParameterTypeValues
pageintfrom 1 (default 1)
limitint1–100 (default 20)
directionenuminbound | outbound
statusenumnew | to_call | contacted | qualified
dateFrom / dateTostringdate YYYY-MM-DD or ISO 8601, e.g. 2026-06-03T12:34:56Z

Request

curl "https://www.heilo.io/api/v1/calls?limit=20&direction=inbound" \
  -H "Authorization: Bearer hk_live_AbC1MnPq..."

Response

{
  "success": true,
  "data": {
    "items": [
      {
        "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "direction": "inbound",
        "caller_phone": "+48600100200",
        "customer_phone_e164": "+48600100200",
        "caller_name": "Jan Kowalski",
        "duration": 87,
        "crm_status": "new",
        "review_status": null,
        "outbound_lifecycle": null,
        "transcript_processed": { "caller_name": "Jan Kowalski", "summary": "...", "service_needed": "..." },
        "created_at": "2026-06-03T12:34:56Z"
      }
    ],
    "has_more": false,
    "page": 1,
    "limit": 20
  },
  "meta": { "timestamp": "2026-06-03T12:34:56Z" }
}
GET/api/v1/calls/:id

Fetch a single call by id. 404 if it doesn't belong to the key's organization or was deleted.

Request

curl https://www.heilo.io/api/v1/calls/<id> \
  -H "Authorization: Bearer hk_live_AbC1MnPq..."

Recording files are NOT exposed via the API — they are erased per legal retention (GDPR). The API returns call metadata and transcript text.

Rate limits

Hourly limits per-key and per-user (sum of all keys). Reset on the UTC hour. Every request counts, regardless of response status.

Per key

1000 req/h

Per account (sum of keys)

5000 req/h

When exceeded we return 429 with Retry-After header (seconds until reset):

HTTP/1.1 429 Too Many Requests
Retry-After: 1842
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 2026-06-03T13:00:00Z

Errors

All errors return a uniform JSON shape with error.code (stable) and error.message (human-readable, may change). Log the code, not the message.

{
  "success": false,
  "error": { "code": "RATE_LIMITED", "message": "Per-key rate limit 1000/h exceeded" },
  "meta": { "timestamp": "2026-06-03T12:34:56Z" }
}
HTTPcodeMeaning
400BAD_REQUESTMalformed query or body parameters (generic validation)
401UNAUTHORIZEDMissing / invalid Bearer token
402SUBSCRIPTION_INACTIVESubscription inactive — renew billing to re-enable the key
403FORBIDDENKey does not have the required scope
404NOT_FOUNDResource not found (or belongs to another user)
422VALIDATION_ERRORA business rule rejected the request (e.g. invalid phone number, quota)
429RATE_LIMITEDHourly limit exceeded (check Retry-After)
500DATABASE_ERRORServer / database error — safe to retry with backoff
503MAINTENANCEPublic API temporarily disabled (kill switch)

The SUBSCRIPTION_INACTIVE code appears in two situations: HTTP 402 — your Heilo subscription has lapsed (billing), and HTTP 409 — the webhook subscription is paused (e.g. on the Test action); in that case click "Reverify" first.

Outbound Webhooks

Heilo POSTs JSON to your endpoint after each event. Subscriptions are created from the "Webhook subscriptions" card — requires a handshake on activation. Every request includes an HMAC signature you must verify:

POST <your URL>
content-type: application/json
heilo-signature: t=1717423396,v1=4f3a...
heilo-event-id: 1bf3a5e2-...
heilo-event-type: call.completed

{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-...",
  "event_type": "call.completed",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-06-03T12:34:56Z",
  "data": { /* see the setup guide for the full schema */ }
}

Retry and pause policy:

  • Transient errors (HTTP 408/429/5xx, network) retry with exponential backoff (2min, 5min, 30min, 2h) — then dead-letter after 5 attempts.
  • Permanent errors (HTTP 401/403/422) pause the subscription immediately — no retries.
  • 50 consecutive transient failures, or 2 consecutive HTTP 410 Gone (e.g. deleted Make scenario), also pause the subscription.
  • Resume a paused subscription with "Reverify" — a fresh handshake reactivates the queue.

When a subscription is paused automatically, we send an e-mail to the account owner. You can resend dead-lettered events with the "Resend" button in the Delivery log — once the subscription passes reverification.

Verification modes

Heilo supports two activation models for webhook subscriptions. The default (permissive) fits Zapier / Make / typical CRM endpoints. Strict matches the Slack Events API model — useful for custom servers that can synchronously echo the challenge.

Mode permissive (default)

Heilo POSTs webhook.subscription.verify to your URL. Any 2xx response activates the subscription. Response body is ignored. This matches the Stripe / GitHub / Twilio model.

Mode strict (opt-in)

Heilo POSTs webhook.subscription.verify with a challenge field. Your endpoint MUST reply with 2xx and a JSON body:

{"challenge":"<echo of the challenge field from Heilo's POST>"}

You pick the mode at subscription creation (verification_mode in POST /api/v1/webhook-subscriptions, default permissive). Changing mode post-creation requires delete + recreate — intentional, as it changes the contract semantics.

Limits: max 20 active subscriptions per account (env-overridable). A subscription is auto-paused after 50 consecutive transient failures or 2 consecutive HTTP 410, and immediately on HTTP 401/403/422.

HMAC verification (signing_secret)

Stripe-compatible format. signed_string = "<unix_ts>.<raw_body>" (dot-separated). Always verify the RAW request body (before JSON parsing) — any normalization changes the signature.

signed_string = "<unix_timestamp>.<raw_request_body>"
signature     = HMAC-SHA256(signing_secret, signed_string).hex()
header        = "t=<unix_timestamp>,v1=<signature>"
import { createHmac, timingSafeEqual } from 'crypto';

// rawBody MUST be the exact received bytes — do NOT re-serialize the JSON.
function verifyHeiloSignature(rawBody, header, signingSecret, toleranceSec = 300) {
  if (!header) return false;
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.split('=').map((s) => s.trim()))
  );
  const t = Number(parts.t);
  const sig = parts.v1;
  if (!Number.isFinite(t) || !sig) return false;

  // Asymmetric tolerance: reject old replays; allow only small clock skew ahead.
  const now = Math.floor(Date.now() / 1000);
  if (now - t > toleranceSec || t - now > 60) return false;

  const expected = createHmac('sha256', signingSecret)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(sig, 'utf8');
  // timingSafeEqual THROWS on length mismatch — length-check first.
  return a.length === b.length && timingSafeEqual(a, b);
}

Default 300s (5 min) tolerance prevents replay attacks. Server clocks must be NTP-synced.

The signing secret is shown only once — when the subscription is created. If you lose it or suspect a leak: delete the subscription and create it again with the same URL (the address in Zapier/Make stays the same). In-place secret rotation is planned.

Event types

Pick event_types when creating a subscription. Each event has a unique event_id (UUID v5) and deduplicates per subscription.

The first event you receive is webhook.test — a test message with data._test = true. Use it to map your fields or filter it out.

event_typeDescription
call.completedCall completed, transcript ready
call.outbound.attemptedOutbound dial attempted (before connect)
call.recording.readyRecording file available for download
call.transcribedTranscript ready (separate from call.completed)
call.failedCall failed (busy/no-answer/error)
call.outbound.lifecycle_repairedOutbound lifecycle correction — call state repaired
call.deletion_scheduledCall scheduled for deletion (GDPR Art. 17, retention)
call.recording.deletedRecording deleted (GDPR)
contact.createdNew contact created
contact.updatedContact updated

Below is the data object of every event. Field names, types and enum values are part of the API contract and do not change meaning within v1; new optional fields may be added over time.

call.completed

Sent after the recording and transcript of a completed call are processed. The primary data source for a CRM — full payload (including recording_url and the processed transcript).

{
  "api_version": "2026-06-15",
  "event_id": "1bf3a5e2-...",        // same value as heilo-event-id header
  "event_type": "call.completed",
  "resource_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "created_at": "2026-06-03T12:34:56Z",
  "data": {
    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "direction": "inbound",
    "caller_phone": "+48600100200",
    "customer_phone_e164": "+48600100200",
    "duration": 87,
    "recording_url": "https://www.heilo.io/api/v1/calls/.../recording.mp3?token=...&exp=...",
    "transcript_processed": { "caller_name": "Jan Kowalski", "summary": "...", "service_needed": "..." },
    "transcript_original": "..."
  }
}
FieldTypeDescription
call_idstring (uuid)Call ID in Heilo — same value as resource_id; links all events of one call.
direction'inbound' | 'outbound'Call direction.
caller_phonestringPhone number of the other party, as stored on the call.
customer_phone_e164stringThe same number again — an alias kept for easier field mapping.
durationnumberRecording length in seconds.
recording_urlstring | nullStable link to the recording; null when the recording was not yet stored at processing time.
transcript_processedobjectProcessed call analysis — see the transcript_processed field reference in this section.
transcript_originalstring | nullRaw verbatim transcript; null when unavailable.

Delivery is at-least-once and not order-guaranteed — persist idempotently. Dedup key: event_id. data.call_id links events of the same call (e.g. call.completed after call.recording.ready).

transcript_processed — field reference

The stable subset you can rely on when mapping to a CRM. Every field is optional — it is null when the call did not contain that information.

FieldTypeDescription
caller_namestring | nullThe caller's name, if they gave one.
summarystring | nullShort paragraph summarising the call.
subjectstring | nullOne-line title of the call (up to 80 characters).
service_neededstring | nullWhat the caller asked for.
services_matchboolean | nullWhether the request matches the services you offer.
lead_scorenumber | null (1–10)Lead quality estimate from 1 to 10.
preferred_datestring | nullDate or time the caller mentioned, if any.
client_citystring | nullCity, if mentioned.
client_addressstring | nullStreet address, if mentioned.
additional_detailsstring | nullExtra context from the call.

Fields that may appear

FieldTypeDescription
caller_locationstring | nullGeographic reference detected in the conversation.
client_countrystring | nullCountry, if mentioned.
counterparty_namestring | nullName of the other party — outbound and conversation-mode calls only.
detected_languagestringLanguage code of the conversation (e.g. pl); the key may be absent entirely.
proposal_itemsobject[] | nullSuggested follow-ups and decisions extracted from the call.

The analysis may include additional fields — treat unknown fields as optional and never assume they are present.

call.outbound.attempted

Sent when an outbound dial reaches a final state — including failures. completed means the call connected and ended normally; the recording and transcript follow as separate events.

FieldTypeDescription
call_idstring (uuid)Call ID in Heilo — same value as resource_id; links all events of one call.
agent_user_idstring (uuid)ID of the Heilo user who placed the call.
customer_phonestringThe dialled customer number.
customer_phone_e164stringThe same number again — an alias kept for easier field mapping.
outbound_lifecycle'completed' | 'agent_no_answer' | 'customer_no_answer' | 'failed_to_initiate'Final state that triggered the event; completed means the call connected and ended normally.
durationnumber | nullCall duration in seconds; null when the call failed at initiation or the duration is not known yet.
attempted_atstring (ISO 8601)When the event was emitted (ISO 8601).
has_recordingbooleantrue only when outbound_lifecycle is completed — the recording then follows as call.recording.ready.

call.recording.ready

Sent when the recording file is available. Use it to fetch or archive the audio.

FieldTypeDescription
call_idstring (uuid)Call ID in Heilo — same value as resource_id; links all events of one call.
recording_urlstring | nullStable link to the recording; in rare cases null when the link could not be generated.
durationnumber | nullDuration in seconds; null when not reported yet.

call.transcribed

Sent in the same processing run as call.completed — it carries only the transcript, without call metadata or the recording link.

FieldTypeDescription
call_idstring (uuid)Call ID in Heilo — same value as resource_id; links all events of one call.
transcript_originalstring | nullRaw verbatim transcript; null when unavailable.
transcript_processedobjectProcessed call analysis — see the transcript_processed field reference in this section.

call.failed

Sent when an outbound call did not complete (no answer, busy, initiation error). Emitted for outbound calls only. Usually not worth creating a lead — log a contact attempt instead.

FieldTypeDescription
call_idstring (uuid)Call ID in Heilo — same value as resource_id; links all events of one call.
outbound_lifecycle'agent_no_answer' | 'customer_no_answer' | 'failed_to_initiate'Which stage of the outbound call failed.
failure_reasonstring | nullMachine-readable failure code (e.g. customer_busy, agent_no_confirmation); can be null.

call.outbound.lifecycle_repaired

Sent when Heilo retroactively corrects the state of an outbound call (a late carrier callback proved the call did connect). Update the call state on your side.

FieldTypeDescription
call_idstring (uuid)Call ID in Heilo — same value as resource_id; links all events of one call.
previous_lifecycle'agent_only'State before the correction; currently always agent_only.
new_lifecycle'completed'State after the correction; currently always completed.
repaired_atstring (ISO 8601)When the correction happened (ISO 8601).

call.deletion_scheduled

GDPR Art. 17: the call is scheduled for deletion. Your CRM should stop using the recording and prepare to delete the data.

FieldTypeDescription
call_idstring (uuid)Call ID in Heilo — same value as resource_id; links all events of one call.
pending_deletion_atstring (ISO 8601)When the data will be permanently deleted (ISO 8601).
reason'consent_not_asked' | 'consent_withdrawn' | 'retention_expired' | 'user_erasure'Why the deletion was scheduled.

call.recording.deleted

GDPR: the recording was deleted — recording_url returns 410. Remove or disable the recording link on your side.

FieldTypeDescription
call_idstring (uuid)Call ID in Heilo — same value as resource_id; links all events of one call.
reasonstring | nullReason recorded when the deletion was scheduled; can be null.
recording_sidstring | nullTwilio recording ID; null when it could not be resolved.
deletion_kind'hard_deleted' | 'twilio_404'hard_deleted = deleted by Heilo; twilio_404 = the file was already gone on Twilio's side.
deleted_atstring (ISO 8601)When the recording was deleted (ISO 8601).

contact.created

Sent when a new contact is created in Heilo. data.contact is a full snapshot of the new contact; notes and tags are not included.

FieldTypeDescription
contactobjectFull snapshot of the new contact (fields below).
contact.idstring (uuid)Contact ID in Heilo.
contact.phonestringThe contact's phone number.
contact.first_namestring | nullnull when not provided.
contact.last_namestring | nullnull when not provided.
contact.emailstring | nullnull when not provided.
contact.companystring | nullnull when not provided.

contact.updated

Sent when a contact is edited. Unlike contact.created, this is not a snapshot: data.diff contains only the changed fields.

FieldTypeDescription
contact_idstring (uuid)ID of the updated contact.
diffobject (partial)Only the fields that changed — keys absent from diff were not modified.

Possible keys in diff: first_name, last_name, email, phone, company, notes, tags

webhook.test

The first message on a new subscription and on every manual test. resource_id is the subscription ID (not a call ID); the remaining data fields mirror the call.completed example with sample values.

FieldTypeDescription
_testtrueAlways true — tells the test message apart from real events.
_messagestringHuman-readable note that this is a test.
_sent_atstring (ISO 8601)When the test was sent (ISO 8601).
Versioning & roadmap

Versioning

Heilo API uses a date-stamped version. Only breaking changes bump the major version (v1 → v2). Adding fields or endpoints is non-breaking.

Current version

v1 · 2026-06-15

The date is the API version identifier (date-stamped), not today's date.

Status

Beta

Backwards-compatible: new response fields, new event_types, new endpoints. Breaking change = new major (v2). Old version supported min. 12 months after v2 announcement.

Roadmap (v1.1+)

Endpoints planned for upcoming v1.X releases. Not a hard commitment — direction depends on feedback.

  • GET /contactslist contacts
  • POST /contactscreate contact (sync from CRM into Heilo)

Need an endpoint? Email support@heilo.io with your use-case — we prioritize the roadmap based on real demand.

Manage keys and webhooks in the panel

Generate API keys, add webhook subscriptions and watch the delivery log after signing in.