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

Every Ringee credential is server-side. Your frontend never talks to Ringee directly. Two server-side entry points are enough:

Secrets

Never expose these in the frontend, in localStorage, in the repository, or in logs. Never send RINGEE_API_KEY from a browser.

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 call.outcome.updated, note.created, callback.created, meeting.created, recording.ready and dnc.created.
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.
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

Ringee sends data.contact.externalId when the record was previously linked. Resolve in this order:
  1. data.contact.externalId — the reliable key.
  2. Normalized phone number, if no externalId is present.
    • For inbound calls, match on fromNumber.
    • For outbound calls, match on toNumber.
  3. No match: create an unlinked activity showing the raw phone number.
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 { "received": true, "event": "test.ping" }.
  • 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.
  • Contact created or updated in Ringee from a CRM change.
  • Contact archived in Ringee from a CRM delete.
  • 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