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 SDKpk_live_… key is browser-safe when restricted to approved origins.
Two server-side entry points are enough:
Configuration and 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 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:data.externalId— when you started an AI Voice Agent call withmetadata.external_idormetadata.externalId.data.contact.externalId— when the contact was linked throughcontact.upsertedand the event includes a contact reference.- Normalized phone number if neither correlation id is 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.
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.
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 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 any2xxresponse.
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.
- Human calls omit
data.agent; AI Voice Agent calls include its{ id, name }. metadata.external_idfrom an AI Voice Agent call is returned asdata.externalIdon its terminal, outcome, callback, meeting and recording events.- A missing
data.userordata.agentnever prevents processing bycallId.
Sync and calling
Sync and calling
- 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
failedand 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
Next steps
Logs and errors
Debug deliveries and inbound events
Dialer SDK
Put the dialer inside your own UI

