Architecture
Every Ringee credential is server-side. Your frontend never talks to Ringee directly. Two server-side entry points are enough:Secrets
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 sendsdata.contact.externalId when the record was previously linked. Resolve in this order:
data.contact.externalId— the reliable key.- Normalized phone number, if no
externalIdis present.- For inbound calls, match on
fromNumber. - For outbound calls, match on
toNumber.
- For inbound calls, match on
- No match: create an unlinked activity showing the raw phone number.
inbound/incoming or outbound/outgoing.
Prevent sync loops
The classic failure: Ringee writes an activity → your CRM sees a row change → your CRM sendscontact.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:
pending→processing→sent|failed. - Retry with backoff; keep
last_errorfor 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 anorganization_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.
Test checklist
Signature and transport
Signature and transport
- 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
eventIdreturns200and creates nothing. test.pingreturns{ "received": true, "event": "test.ping" }.
Call mapping
Call mapping
- 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.completedstill lands on one activity. - Outcome received after
call.completedupdates, never duplicates. - Recording received later attaches to the existing activity.
Sync and calling
Sync and calling
- 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
Next steps
Logs and errors
Debug deliveries and inbound events
Dialer SDK
Put the dialer inside your own UI

