Skip to content
RelationsAI
Guides

Integrating with CRM systems

Preview

The most common integration connects RelationsAI to a CRM: a new or updated lead triggers a call, and the call's outcome, summary and transcript flow back to the lead record. This guide describes that pattern end to end.

It works with any CRM that can send an HTTP request (or be polled) when a record changes, and that you can write to through its own API. Nothing here depends on a particular CRM product.

Before you begin

You needFor
A live key and a test key, each with contacts:write, calls:write, calls:read, analysis:read, dnc:writeYour integration service (one key per mode)
transcripts:read (optional)Copying transcripts to the record
A webhook endpoint subscribed to call.ended and call.settledWriting results back (Receiving webhooks)
A ready agentThe calls (Creating and preparing agents)

Keep the API key in your integration service, never in the CRM's browser-side scripts: live keys are refused from browsers.

The architecture

Text
CRM record changes ──▶ your integration service 1. compliance first: do-not-call flag ─▶ POST /dnc-entries, stop 2. POST /contacts/upsert (name, consent, custom fields) 3. POST /calls (metadata: crm_record_id, crm_event_id; skipped if this CRM event id was handled) RelationsAI ──webhook──▶ your integration service 4. call.ended ─▶ status on the record ("No answer", "Busy") 5. call.settled ─▶ GET /calls/{id}/analysis ─▶ summary + fields to the record Every hour ─▶ 6. reconcile: GET /calls?created_after=... ─▶ anything the webhooks missed

Your integration service is the only component that holds RelationsAI keys and the CRM's credentials. It records which CRM events it has handled and which calls it has written back, so every step can safely run twice.

Sync the contact

Upsert the contact by phone number with the fields the agent needs (POST/contacts/upsert). Map CRM fields to the contact's name, language and custom fields (defined in the console), so the agent can use them as {{field_name}}. Send consent_granted only as your CRM records it.

Trigger the call

Place the call with the CRM record id and the CRM event id in metadata. CRM automations commonly fire twice for one change, and webhook deliveries from your CRM can be retried too, so record each CRM event id you handled and skip one you have already seen. Every request to RelationsAI is executed, so this check is what prevents a second call. You can also ask RelationsAI: list the calls to that number from the last day (GET/calls with to and created_after) and look for one whose metadata.crm_event_id matches, as the complete example does. For extra safety you can also send an optional Idempotency-Key derived from the CRM event, for example crm-<event id>-call: a duplicate then replays the first answer (see Idempotency).

One CRM event becomes this sequence of requests (compliance, upsert, call):

1. Upsert the contact from the CRM record. Send consent_granted only as the CRM records it:

curl -X POST https://voice-api.relationsai.com/v1/contacts/upsert \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+12025550100", "name": "Asha" }'

2. Place the call with the CRM ids in metadata. Handle each CRM event id once; an optional Idempotency-Key such as crm-E-9001-call adds a second guard:

curl -X POST https://voice-api.relationsai.com/v1/calls \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "agent_id": "agent_1ZltiWzxrXmMprGD6q63", "to": "+12025550100", "variables": { "first_name": "Asha" }, "metadata": { "crm_record_id": "L-1042", "crm_event_id": "E-9001" } }'

If the CRM marks the person do-not-call, send this instead of steps 1 and 2, and place no call:

curl -X POST https://voice-api.relationsai.com/v1/dnc-entries \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+12025550100" }'
Complete runnable example

Turn one CRM event into a contact update and a call, safely repeatable.

The same steps as a complete script, with error handling, tested against test mode on every docs build. Each script reads your test key from an environment variable named in its first lines.

Decisions in this flow:

  • Compliance first. If the CRM marks the person do-not-call, add the number to the do-not-call list and stop. Never call to "confirm" an opt-out.
  • A refusal is an outcome, not an error to retry. retry: "no" errors (for example dnc_listed, consent_required, outside_calling_hours) are written to the CRM record as the result of the attempt. Retry only retry: "yes" or "after" errors, and after a timeout check whether the call exists (by metadata) before resending.
  • A 202 is not a result. Write "call queued" to the record if you like; the outcome comes from the webhooks.

For many records at once (a nightly list, a segment), use a campaign instead of a loop of single calls: see Common integration patterns.

Write the outcome back

On call.ended, write the outcome early if your users want to see "No answer" or "Busy" quickly. On call.settled, everything is final:

  1. Read data.object.metadata.crm_record_id from the event. No lookup table needed.
  2. Fetch GET/calls/{call_id}/analysis, and the transcript if you copy it.
  3. Write to the record: outcome, duration, the summary, each ready extracted field mapped by name, goal_met, and the call id (so a person can trace the values back to the conversation).
  4. Record the call id as written back. A duplicate call.settled then changes nothing.

The webhook receiver does steps 1 and 2 on call.settled; replace its log line with your CRM write. Put the CRM write in the background step, never before answering the webhook.

What to write where is your choice, but some advice:

  • Store transcripts and recordings only if your users need them and your data-protection obligations allow it. A link back to your own copy is often better than pasting a whole transcript into a note.
  • Never store the recording url: it expires in about 15 minutes and works without a key. Download the audio instead.
  • Write ready analysis values only; do not overwrite CRM data with absent (null) values.

Keep compliance in sync

Do-not-call and consent must be the same in both systems, in both directions:

ChangeDirectionDo
The person opts out in the CRMCRM to RelationsAIPOST/dnc-entries immediately, before any further call
Consent recorded or withdrawn in the CRMCRM to RelationsAIUpsert the contact with consent_granted: true or false
A call is refused with dnc_listedRelationsAI to CRMMark the record do-not-call
Someone is added to the list in the consoleRelationsAI to CRMPeriodically list GET/dnc-entries and mark new numbers in the CRM

Never remove a do-not-call entry because the CRM still shows the person as callable. A removal is a deliberate act with an audit trail (see Consent and DNC).

Reconcile

Webhooks can be missed: your endpoint was down for longer than the retry window, a deploy broke verification, an event was acknowledged but not processed. Run a scheduled job that lists recent calls (GET/calls with created_after and status=ended), and writes back any settled call that is not in your written-back table. The reconciliation example does exactly that.

Test to live

  1. Build and test everything with a test key and a test-mode webhook endpoint. Test contacts, calls and endpoints never mix with live ones.
  2. Use the test numbers to exercise each branch: +12025550100 (completed, with analysis), +12025550101 (no answer), +12025550103 (failed), and a number you add to the test-mode do-not-call list (refused).
  3. Create a live key with the same scopes and a live webhook endpoint (it has its own secret). Switch your service's configuration, not its code.
  4. Start with a small set of real records and watch the first results in both systems. See Going live.

Ids are mode-specific: a ct_... or call_... id from test mode does not exist in live mode. Do not copy stored ids from your test setup into production data.

What can go wrong

  • Two calls for one CRM change: The same CRM event handled twice, or a blind retry after a timeout. Fix: Record handled CRM event ids; check for the call by metadata before resending; optionally send a key derived from the CRM event id.
  • idempotency_key_reused (only if you send keys): One key used for two different requests (for example two steps sharing one key). Fix: Add the step to the key (crm-<event>-contact, crm-<event>-call).
  • Results written to the wrong record: Matching by phone number instead of metadata. Fix: Put the record id in metadata and match on it.
  • Analysis fields missing in the CRM: Written on call.ended before they were ready. Fix: Write analysis on call.settled.
  • The CRM shows a person callable who opted out by phone: Refusals not written back. Fix: Mark dnc_listed refusals and new do-not-call entries in the CRM.
  • rate_limited during a bulk sync: Unpaced loops. Fix: Pace writes and honour Retry-After.

Next steps

Common integration patterns.