For the complete documentation index, see llms.txt. This page is also available as Markdown.

๐Ÿ”Œ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.

You want toโ€ฆ
Use
Code needed?

Capture leads from your own or a client's website form

No (tracking script)

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

  1. An Agency-plan workspace โ€” the CRM Postback API is Agency-only.

  2. An API key, created at Settings โ†’ API Keys โ†’ Create API Key (copy it once โ€” it's shown only once).

  3. 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 contacts

    • crm:read โ€” to look up contacts or read a contact's event log

    • crm:force โ€” only if you'll ever move a contact backward in the funnel

  4. The base URL: https://ringtonic.app/api/v1

The full ability reference is in Authentication below.


Quickstart

That single call:

  1. Finds the existing Contact with phone +15551234567 in your workspace

  2. Records a conversion event with stage appointment_booked

  3. Advances the Contact's funnel stage (if forward)

  4. Stores deal_value of $5,000.00

  5. Fires conversion.recorded and contact.stage_changed webhooks

  6. Queues 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.

Creating a CRM API key under Settings โ†’ API Keys, with the workspace and crm abilities

Required header:

Required token abilities when creating the key:

Ability
Grants

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


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_reuse

  • Same 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, OR target_contact_id

  • stage โ€” any value from the Contact stages enum (new, contacted, form_submitted, qualified, appointment_booked, proposal_sent, won, customer, lost, unqualified)

Optional fields:

Field
Purpose

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 201

  • Existing contact matched by external_id โ†’ returns 409 external_id_exists with the existing contact_id

  • Existing contact matched uniquely by phone or email โ†’ returns 200 with the existing contact (idempotent on identifier match)

  • Multiple matches โ†’ 422 ambiguous_match with candidates array

  • Soft-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 match

  • 404 { "match": null } โ€” no match

  • 422 { "error": "ambiguous_match", "candidates": [ ... ] } โ€” multiple contacts matched, send target_contact_id on 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

Code
Meaning

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

Endpoint
Limit

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):

Webhook event
Fires on

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.


  • 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