Resource
Contacts
People you can reach. Upsert is idempotent on email or phone — calling it twice with the same identity returns the existing contact, no duplicates.
Tenancy
Every contact lives at the intersection of
org_id and client_id. With a gc_live_ key, you're writing to the agency's own CRM unless you pass X-Gary-Client-Id. With cl_live_, you're always writing to that bound client's CRM.List contacts#
List contacts
GET
/v1/crm/contactsCursor-paginated list. Defaults to 50 per page, max 200.
Query parameters
limitintegerdefault: 50Maximum items per page (1–200).
page_tokenstringOpaque cursor returned in next_page_token from the prior page.
qstringSubstring match across full_name / first_name / last_name.
lifecyclestringFilter by lifecycle_stage (e.g.
lead, qualified).owneruuidRestrict to contacts owned by this user_id.
Request
curl "https://agency.gary.club/api/public/v1/crm/contacts?limit=10&q=jane" \
-H "Authorization: Bearer gc_live_EXAMPLE_AbCdEfGhIj0123456789"Create / upsert a contact#
Idempotent upsert by email or phone
POST
/v1/crm/contactsEither
email or phone is required (one of). If a contact already exists with that identity, we merge in the new fields and return created: false instead of inserting a duplicate.Body parameters
emailstringIdentity. Required if phone is not provided.
phonestringE.164. Required if email is not provided.
first_namestringGiven name.
last_namestringFamily name.
full_namestringUse this only when you can't split first/last.
titlestringJob title.
company_iduuidLink this contact to a company you've already created.
company_namestringIf you don't have a company_id, we'll resolve / create one by domain.
sourcestringFree-form attribution (e.g.
n8n_inbound_form).Request
curl -X POST https://agency.gary.club/api/public/v1/crm/contacts \
-H "Authorization: Bearer gc_live_EXAMPLE_AbCdEfGhIj0123456789" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"title": "Head of Ops",
"source": "n8n_inbound_form"
}'Successful creation returns:
Response
{
"contact": {
"id": "con_EXAMPLE_aaaa",
"client_id": "00000000-0000-0000-0000-000000000000",
"full_name": "Jane Doe",
"first_name": "Jane",
"last_name": "Doe",
"title": "Head of Ops",
"lifecycle_stage": "lead",
"company_id": null,
"owner_user_id": null,
"tags": [],
"created_at": "2026-04-27T12:34:56.000Z",
"updated_at": "2026-04-27T12:34:56.000Z"
},
"created": true
}Read one#
Read a single contact
GET
/v1/crm/contacts/{id}Full contact record with hydrated company link.
Path parameters
iduuidrequiredContact id.
Request
curl https://agency.gary.club/api/public/v1/crm/contacts/con_EXAMPLE_aaaa \
-H "Authorization: Bearer gc_live_EXAMPLE_AbCdEfGhIj0123456789"Update#
Update mutable fields
PATCH
/v1/crm/contacts/{id}Pass any subset of mutable fields. Sending
archived: true sets the soft-delete timestamp (the row stays for tenancy triggers).Path parameters
iduuidrequiredContact id.
Body parameters
first_namestringlast_namestringtitlestringlifecycle_stagestringe.g.
lead, qualified, customer.company_iduuid | nullRe-link or unlink the company.
owner_user_iduuid | nullReassign ownership.
tagsstring[]Replaces the entire tags array. Read-modify-write to add a tag.
archivedbooleanSoft-delete shortcut.
true sets archived_at; false clears it.Request
curl -X PATCH \
https://agency.gary.club/api/public/v1/crm/contacts/con_EXAMPLE_aaaa \
-H "Authorization: Bearer gc_live_EXAMPLE_AbCdEfGhIj0123456789" \
-H "Content-Type: application/json" \
-d '{ "lifecycle_stage": "qualified", "tags": ["warm","followup-q4"] }'Archive#
Soft-archive
DELETE
/v1/crm/contacts/{id}Sets archived_at to now. The row is preserved for tenancy triggers and historical activity timelines; un-archive via PATCH archived=false.
Path parameters
iduuidrequiredContact id.
Request
curl -X DELETE \
https://agency.gary.club/api/public/v1/crm/contacts/con_EXAMPLE_aaaa \
-H "Authorization: Bearer gc_live_EXAMPLE_AbCdEfGhIj0123456789"Phones & emails (sidecar tables)#
A contact can have multiple phones and emails. They live in 1:N sidecar tables and have their own CRUD endpoints — use these to add a second phone, mark a number as DNC, set consent state, or change the primary email without touching the contact row.
List phones
GET
/v1/crm/contacts/{id}/phonesAll phones attached to the contact, ordered with the primary first.
Path parameters
iduuidrequiredContact id.
Add phone
POST
/v1/crm/contacts/{id}/phonesIdempotent on
phone_e164 — re-posting the same number returns the existing row with created: false. The first phone added is auto-marked as primary; pass is_primary: true to re-anchor.Path parameters
iduuidrequiredBody parameters
phonestringrequiredE.164 (+15551234567). Normalized server-side.
phone_typestringmobile | work | home | other (defaults to mobile)
is_primarybooleanIf true, demotes any existing primary first.
consent_statusstringunknown | opt_in | opt_out | do_not_call
consent_sourcestringHow consent was captured (e.g. signup_form_v2).
sourcestringFree-form provenance label.
Read phone
GET
/v1/crm/contacts/{id}/phones/{phoneId}Path parameters
iduuidrequiredContact id.
phoneIduuidrequiredPhone row id.
Update phone
PATCH
/v1/crm/contacts/{id}/phones/{phoneId}Patch any subset. Toggling is_primary auto-demotes the previous primary.
Path parameters
iduuidrequiredphoneIduuidrequiredBody parameters
phone_typestringis_primarybooleanstatusstringunverified | verified | invalid
consent_statusstringconsent_sourcestringconsented_atstring (ISO 8601)dnc_scopestringindividual | company | global
Delete phone
DELETE
/v1/crm/contacts/{id}/phones/{phoneId}Hard-delete the phone row. Returns 204.
Path parameters
iduuidrequiredphoneIduuidrequiredThe same five endpoints exist for emails at /v1/crm/contacts/{id}/emails and /v1/crm/contacts/{id}/emails/{emailId}. Body shape is the same with email instead of phone and phone_type replaced by status (unverified, verified, bounced).
Webhooks for contacts#
contact.created— first time we've seen this identity.contact.updated— any subsequent change to mutable fields, OR a phone/email added/changed.contact.archived— soft-archive via DELETE.
Subscribe via /v1/webhooks to react in n8n, Claude, or your own service.

