๐CRM Postback API
The CRM API lets external systems โ your existing CRM, Zapier, Calendly, n8n, or custom integrations โ push conversion events back into Ring Tonic. Every event you push appears on the matching Contact's timeline, advances the funnel, and (when configured) uploads to Google Ads as an Enhanced Conversion.
The CRM Postback API is Agency plan only. The general API key feature (API) is also Agency-gated.
Who this is for
For developers โ jump to Quickstart and Endpoints: request/response bodies, status codes, idempotency, rate limits, and ready-to-paste recipes for Calendly, HubSpot, and Zapier.
For everyone else โ this connects an outside system (your CRM, scheduler, or automation tool) to Ring Tonic, so that when a lead books a call or a deal closes elsewhere, it lands on the contact's timeline and pushes the conversion to Google Ads automatically. It needs a developer โ or a tool like Zapier โ to set up once. If you just want to capture leads from your own website, the no-code Form Submissions is usually the better fit (see below).
Do I need the API?
Reach for the Postback API only when the event happens in a system Ring Tonic can't already see.
Carry attribution into the client's existing CRM
Form Attribution
No (tracking script)
Push events from an external CRM, scheduler, or automation (HubSpot, Calendly, Zapier, custom)
CRM Postback API (this page)
Yes (a developer or Zapier)
What you need before you start
An Agency-plan workspace โ the CRM Postback API is Agency-only.
An API key, created at Settings โ API Keys โ Create API Key (copy it once โ it's shown only once).
The right abilities on that key:
workspace:<id>โ exactly one, the workspace this key may act on (required)crm:writeโ to record conversions and create or update contactscrm:readโ to look up contacts or read a contact's event logcrm:forceโ only if you'll ever move a contact backward in the funnel
The base URL:
https://ringtonic.app/api/v1
The full ability reference is in Authentication below.
Quickstart
That single call:
Finds the existing Contact with phone
+15551234567in your workspaceRecords a conversion event with stage
appointment_bookedAdvances the Contact's funnel stage (if forward)
Stores
deal_valueof$5,000.00Fires
conversion.recordedandcontact.stage_changedwebhooksQueues a Google Ads Enhanced Conversion upload (if a mapping is configured)
Authentication
All endpoints require a Sanctum personal access token. Generate one at Settings โ API Keys โ Create API Key.

Required header:
Required token abilities when creating the key:
workspace:<id>
Limits the token to one specific workspace โ exactly one workspace scope is required
crm:read
GET /api/v1/contacts/match, GET /api/v1/contacts/{id}/events
crm:write
POST /api/v1/contacts, PATCH /api/v1/contacts/{id}, POST /api/v1/conversions
crm:force
Required when sending force_stage: true to move a contact backwards
A CRM token must be scoped to exactly one workspace. Tokens without a workspace:<id> ability โ or with multiple workspace abilities โ receive 401 token_missing_workspace_scope / token_multiple_workspace_scopes. Mint a separate token per workspace.
Rule of thumb: workspace-scope problems return 401; a valid token that's simply missing a crm:* ability returns 403.
Idempotency
Every state-changing endpoint accepts an optional Idempotency-Key header. Retries with the same key + same body return the original response and skip side effects.
Recommended format: UUID v4 or any unique value your system can re-generate on retry.
Same key, same body within 24h โ cached response (200, not 201, on replay)
Same key, different body within 24h โ 409
idempotency_key_reuseSame key on a previously-failed request โ cached failure response (mint a new key to retry against the same input)
Idempotency works equally for POST /contacts and POST /conversions. The window is 24h.
Identity Matching
Several endpoints accept phone, email, and external_id as ways to identify a contact. Ring Tonic's matcher uses priority order with conflict detection:
The matcher tries the highest-priority supplied identifier first. If a lower-priority identifier points to a different contact, the request is rejected with 422 conflicting_identifiers and a candidates array โ this prevents a stale email from silently routing a conversion to the wrong contact.
Resolving conflicts: add target_contact_id to your request body to tell the matcher exactly which contact you mean. Hint identifiers are still soft-validated (a mismatch returns 422 target_identifier_mismatch), but the named contact is used.
Endpoints
POST /api/v1/conversions โ record a conversion event
POST /api/v1/conversions โ record a conversion eventThe primary postback endpoint. Use this whenever an external event has occurred that should advance the funnel: appointment booked, proposal sent, deal won, etc.
Request body:
Required fields:
At least one identifier โ
phone,email,external_id, ORtarget_contact_idstageโ any value from the Contact stages enum (new,contacted,form_submitted,qualified,appointment_booked,proposal_sent,won,customer,lost,unqualified)
Optional fields:
value_cents
Deal value in cents. Required when stage is won or proposal_sent (or send inherit_value: true if the contact already has one set)
inherit_value
true to satisfy the won / proposal_sent value requirement by reusing the contact's existing deal value, instead of sending value_cents
currency_code
ISO 4217 โ defaults to workspace currency
occurred_at
When the event happened in your system (defaults to server time)
force_stage
true to allow a backward stage move. Requires crm:force token ability
create_if_missing
true to create a new Contact when no match is found (otherwise 404)
target_contact_id
Skip the matcher and use this contact id directly
custom_fields
Map of key: value matching workspace custom field schema
external_event_id
Your system's identifier for this specific event (useful for support cross-referencing)
Response 201 (new event):
Response 200 โ idempotent replay (same body as the original 201).
Response when stage was not applied (e.g. backward move without force):
In the no-op case the event still appears in the contact's timeline as audit history; conversion.recorded fires but contact.stage_changed does NOT.
POST /api/v1/contacts โ create or upsert a contact
POST /api/v1/contacts โ create or upsert a contactUse when you want to create a contact explicitly (without sending a conversion event yet).
Request body:
Behavior:
No existing contact โ creates one and returns
201Existing contact matched by
external_idโ returns409 external_id_existswith the existingcontact_idExisting contact matched uniquely by phone or email โ returns
200with the existing contact (idempotent on identifier match)Multiple matches โ
422 ambiguous_matchwithcandidatesarraySoft-deleted contact matches โ restored and returned with
"restored": true
Phone numbers are normalized to E.164. custom_fields are validated against your workspace schema; unknown keys are rejected unless you pass ?allow_unknown_fields=true (which stashes them under a _unmapped sub-key).
PATCH /api/v1/contacts/{id} โ update a contact
PATCH /api/v1/contacts/{id} โ update a contactPartial update. Body accepts any subset of the create-contact fields.
You can also move the contact's stage here by sending lead_status. Note the field name differs by endpoint: POST /conversions uses stage, while this endpoint uses lead_status. The same forward-only rules apply, and a conversion event is recorded with source postback.
GET /api/v1/contacts/match โ diagnostic lookup
GET /api/v1/contacts/match โ diagnostic lookupReturns the contact the conversion endpoint would resolve for the given identifiers. Useful when wiring up Zapier or n8n to test your match logic before going live.
Responses:
200 { "match": { "id": 1284, ... }, "matched_by": "phone" }โ unique match404 { "match": null }โ no match422 { "error": "ambiguous_match", "candidates": [ ... ] }โ multiple contacts matched, sendtarget_contact_idon your conversion call to pick one
GET /api/v1/contacts/{id}/events โ conversion event log
GET /api/v1/contacts/{id}/events โ conversion event logReturns the conversion-event timeline for one contact โ every stage change, with where it came from, its value, and when it happened. Cursor-paginated, 50 per page; follow next_cursor for older events.
Response 200:
Use it to sync a contact's funnel history back to your CRM.
Status Codes
200
Idempotent replay, or unique-identifier match returning existing contact
201
New event recorded / new contact created
400
Malformed body (invalid JSON, missing required structure)
401
Token missing, invalid, or lacks workspace:<id> scope
403
Workspace not on Agency plan, or token lacks crm:write / crm:force
404
No matching contact (and create_if_missing=false), or contact in another workspace
409
external_id_exists (POST /contacts), or Idempotency-Key reused with a different body
422
Validation failed, or ambiguous_match / conflicting_identifiers / target_identifier_mismatch
429
Rate limit (see below)
Error responses carry a JSON body with an error code (the names used in the rows above) plus any useful context:
Rate Limits
POST /api/v1/conversions
120 requests / minute / workspace
POST /api/v1/contacts
60 requests / minute / workspace
PATCH /api/v1/contacts/{id}
120 requests / minute / workspace
GET /api/v1/contacts/match
60 requests / minute / workspace
GET /api/v1/contacts/{id}/events
60 requests / minute / workspace
All limits are keyed per workspace. 429 responses include a Retry-After header โ back off and retry after the window resets. (Idempotent replays still pass through the limiter, so count them in your budget.)
Webhook Companions
Every postback fires one or more outbound webhooks (see Webhooks):
conversion.recorded
Every accepted conversion event (including no-op audit events)
contact.stage_changed
Only when the contact's stage actually moved
form.submitted
Only on form-capture beacons (not on postback API calls)
The ordering inside a transaction is deterministic: form.submitted โ conversion.recorded โ contact.stage_changed.
Integration Recipes
Calendly: appointment booked
In Calendly's webhook settings, point at https://ringtonic.app/api/v1/conversions with body:
Use external_event_id as your Idempotency-Key so Calendly's retry-on-failure doesn't double-book the funnel.
HubSpot: deal closed-won
Set up a workflow that fires when a deal stage = "Closed Won" and call:
The external_id matched on the contact's HubSpot ID gives you stable identity across CRM updates.
Zapier: form-fill in a non-Ring-Tonic form
Use a Zap with Code by Zapier to generate an Idempotency-Key (UUID), then POST to /api/v1/conversions with:
For Ring Tonic-hosted tracking, use Form Submissions instead โ it's faster and free of Zapier latency.
Common Questions
Do I need a separate API key per integration?
It's a good idea โ separate keys can be revoked independently if one integration is compromised. Each key needs the same workspace:<id> ability for the target workspace.
What's the difference between `external_id` and `target_contact_id`?
external_id is your system's stable identifier for the contact (e.g. HubSpot's hs_object_id). It's stored on the Contact and used for future matching. target_contact_id is Ring Tonic's internal contact ID โ useful as an escape hatch when the matcher returns conflicting_identifiers and you need to disambiguate.
Can I push the same event twice without dedup?
Yes โ omit Idempotency-Key. Each call creates a new ConversionEvent. The contact still only advances on the first forward-stage transition; subsequent events are logged as (ignored) no-ops. Most integrators want idempotency on; the option to skip it is there for systems that genuinely need a separate event per call.
How long are idempotency records kept?
24 hours from the original request. After that the key is reusable.
Why am I getting 401 even though my key is correct?
The most common cause is a missing or incorrect workspace ability. CRM endpoints require workspace:<id> matching the workspace you want to act on. Re-create the key under Settings โ API Keys with explicit workspace:<id>, crm:write (and crm:force if needed). Legacy un-scoped keys created before the CRM API shipped will fail closed on these routes with an upgrade hint.
Related Guides
API โ general API access (keys, GeoData endpoint)
Contacts โ pipeline view of the data this API writes
Form Submissions โ Ring Tonic's own no-code form capture
Webhooks โ subscribe to events emitted by postbacks
Last updated