Skip to main content
This guide is the shape of a production-grade Ringee integration: what to build, in what order, and the failure modes that bite everyone the first time.

Architecture

The Custom Integration API key and webhook signing secret are server-side. Your frontend does not call the Public API directly. The separate Dialer SDK pk_live_… key is browser-safe when restricted to approved origins. Two server-side entry points are enough:

Configuration and secrets

Never expose RINGEE_API_KEY or RINGEE_WEBHOOK_SIGNING_SECRET in the frontend, in localStorage, in the repository, or in logs.

Build order

1

Receive and verify webhooks

Start here. It is the piece most likely to be wrong, and test.ping gives you a tight feedback loop with no data at stake. See Outbound webhooks.
2

Add the idempotency table

Insert eventId with a UNIQUE constraint before processing. On conflict, return 200 and stop.
3

Map call events to activities

Handle call.completed, call.missed and call.failed, keyed by data.callId. Then handle call.outcome.updated, note.created, callback.created, meeting.created, recording.ready and dnc.created. call.outcome.updated includes data.call when available, so it can upsert the activity even if it arrives first.
4

Sync contacts and companies to Ringee

Send contact.upserted and company.upserted when the relevant fields change — through an outbox, not inline with the save.Read status on every response, and log warnings: they are how you catch a field name Ringee does not know, or an ownerEmail that matches nobody. To enrol a synced contact in an outbound campaign in the same request, add data.campaignId.
5

Add the call button

Wire click-to-call, or embed the Dialer SDK if you want agents to stay in your UI.
6

Add the settings screen

Expose status, the webhook URL to paste into Ringee, last sync, last error, Test connection and Retry pending — without ever showing a secret.

Matching contacts

Resolve an event to your record in this order:
  1. data.externalId — when you started an AI Voice Agent call with metadata.external_id or metadata.externalId.
  2. data.contact.externalId — when the contact was linked through contact.upserted and the event includes a contact reference.
  3. Normalized phone number if neither correlation id is present.
    • For inbound calls, match on fromNumber.
    • For outbound calls, match on toNumber.
  4. No match: create an unlinked activity showing the raw phone number.
data.agent.id is a Ringee AI Voice Agent id. Do not use it as a contact id. data.user.id identifies the Ringee user the event belongs to.
Never create a duplicate contact from a call event. An unlinked activity is always better than a duplicate record.
Normalize direction for display — the wire values may be inbound/incoming or outbound/outgoing.

Prevent sync loops

The classic failure: Ringee writes an activity → your CRM sees a row change → your CRM sends contact.upserted → Ringee updates the contact → repeat.
1

Tag Ringee-written rows

Set sync_origin = 'ringee' on anything a Ringee webhook created or updated.
2

Skip those rows on the way out

Your CRM → Ringee sync must ignore:
  • changes where sync_origin = 'ringee';
  • changes limited to call-activity fields;
  • recordings;
  • outcomes.
3

Sync on meaningful changes only

Compare the fields Ringee actually uses. Do not emit an event because a timestamp or a UI-only column moved.

Outbox and retries

A contact save must never fail because Ringee is briefly unreachable. Record the event first, deliver second.
  • States: pendingprocessingsent | failed.
  • Retry with backoff; keep last_error for the settings screen.
  • Offer an administrative Retry action.
  • Never send two events for the same record concurrently — you will apply them out of order.

Multi-tenancy

If your CRM is multi-tenant, every new table needs an organization_id (or equivalent) and row-level security:
  • users may read only their own organization’s rows;
  • integration settings and retry actions are limited to authorized users;
  • payloads, recordings, activities and logs are never readable across organizations.
Your webhook function may use administrative database credentials — but only after the signature has been verified.

Test checklist

  • Valid HMAC signature is accepted.
  • Invalid signature returns 401.
  • Expired timestamp (more than 5 minutes old) is rejected.
  • Body modified after signing is rejected.
  • Duplicate eventId returns 200 and creates nothing.
  • test.ping returns any 2xx response.
  • Inbound completed call creates an activity on the right contact.
  • Outbound completed call creates an activity on the right contact.
  • Missed call is flagged as missed.
  • Failed call stores the error code or message.
  • Outcome received before call.completed still lands on one activity.
  • Outcome received after call.completed updates, never duplicates.
  • Recording received later attaches to the existing activity.
  • Human calls omit data.agent; AI Voice Agent calls include its { id, name }.
  • metadata.external_id from an AI Voice Agent call is returned as data.externalId on its terminal, outcome, callback, meeting and recording events.
  • A missing data.user or data.agent never prevents processing by callId.
  • Contact created or updated in Ringee from a CRM change.
  • Contact archived in Ringee from a CRM delete.
  • A payload with a bad field returns failed and leaves nothing behind in Ringee.
  • A clean payload returns no warnings.
  • Click-to-call works for an authenticated user.
  • Contact without a phone, or on DNC, cannot be called.
  • No cross-organization data leaks.

What Ringee does not send

Ringee notifies the terminal result of a call. There are no call.started, call.ringing or call.answered events — do not build a real-time incoming-call screen on top of this API. For live call state in your own UI, use the Dialer SDK, which emits dialing, ringing, answered and ended in the browser.

Next steps

Logs and errors

Debug deliveries and inbound events

Dialer SDK

Put the dialer inside your own UI