🔌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.
Who this is for
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
The 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
Use 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
Partial 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
Returns 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
Returns 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
Common Questions
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