Skip to content

Contacts

Contacts are studio members for a location. Create them with POST /api/v2/contacts (manage scope), list the directory with GET /api/v2/contacts (summary scope), update email/name with PATCH /api/v2/contacts/{id} (manage scope), or let a grant create them on first payment. Per-member read endpoints require the summary scope and take location_id as a query parameter. List endpoints accept optional ?limit= (default 20, max 100).

The path {id} is the member’s external contact key — the same value you send as external_contact_id or ghl_contact_id (not Kotally’s internal contact.id). Responses include both external_contact_id and ghl_contact_id (same value) plus the internal id.

POST /api/v2/contacts

Requires the manage scope. Idempotent via request_id.

FieldTypeRequiredNotes
location_idstringyesMust match the API client’s location
request_idstringyesIdempotency key
emailstringyesMember email
namestringnoDisplay name
external_contact_idstringnoYour CRM / booking id (preferred)
ghl_contact_idstringnoAlias; external_contact_id wins if both are set

If neither contact id is sent, Kotally generates a kota-native:… id.

Terminal window
curl -X POST "https://app.<your-domain>/api/v2/contacts" \
-H "Authorization: Bearer ktly_<your-token>" \
-H "Content-Type: application/json" \
-d '{
"location_id": "loc_1",
"request_id": "member-crm_123",
"email": "[email protected]",
"name": "Member Example",
"external_contact_id": "crm_123"
}'
{
"ok": true,
"reason_code": "contact_created",
"correlation_id": "a1b2c3d4-...",
"contact": {
"id": "kotally-contact-uuid",
"ghl_contact_id": "crm_123",
"external_contact_id": "crm_123",
"email": "[email protected]",
"name": "Member Example"
}
}
reason_codeHTTPMeaning
CONFLICT409A member with this contact id already exists for the location
VALIDATION_ERROR400Missing email / location / request_id
BILLING_SUSPENDED403Workspace billing is suspended
GET /api/v2/contacts

Requires the summary scope. Pass location_id as a query parameter. Optional ?q= searches name, email, or external id. Pagination uses an opaque ?cursor= from next_cursor in the previous response.

Terminal window
curl "https://app.<your-domain>/api/v2/contacts?location_id=loc_1&q=jane" \
-H "Authorization: Bearer ktly_<your-token>"
{
"ok": true,
"location_id": "loc_1",
"contacts": [
{
"id": "kotally-contact-uuid",
"external_contact_id": "crm_123",
"ghl_contact_id": "crm_123",
"email": "[email protected]",
"name": "Member Example",
"created_at": "2026-04-01T00:00:00.000Z",
"updated_at": "2026-04-16T00:00:00.000Z"
}
],
"next_cursor": null
}
PATCH /api/v2/contacts/{id}

Requires the manage scope. Path {id} is the external contact key. Send location_id in the body. Update email and/or name only — the external id cannot be changed. Optional request_id enables idempotent retries.

FieldTypeRequiredNotes
location_idstringyesMust match the API client’s location
emailstringnoNew email
namestringnoNew display name
request_idstringnoIdempotency key for safe retries
Terminal window
curl -X PATCH "https://app.<your-domain>/api/v2/contacts/crm_123" \
-H "Authorization: Bearer ktly_<your-token>" \
-H "Content-Type: application/json" \
-d '{
"location_id": "loc_1",
"email": "[email protected]",
"name": "Member Renamed"
}'
{
"ok": true,
"reason_code": "contact_updated",
"correlation_id": "a1b2c3d4-...",
"contact": {
"id": "kotally-contact-uuid",
"external_contact_id": "crm_123",
"ghl_contact_id": "crm_123",
"email": "[email protected]",
"name": "Member Renamed",
"created_at": "2026-04-01T00:00:00.000Z",
"updated_at": "2026-04-16T12:00:00.000Z"
}
}
reason_codeHTTPMeaning
NOT_FOUND404No member with this external id for the location
VALIDATION_ERROR400Missing location_id, no fields to update, or attempt to change external id
BILLING_SUSPENDED403Workspace billing is suspended
GET /api/v2/contacts/{id}/summary

Returns a snapshot of credits, entitlements, recent ledger, appointments, and payments.

Terminal window
curl "https://app.<your-domain>/api/v2/contacts/crm_123/summary?location_id=loc_1" \
-H "Authorization: Bearer ktly_<your-token>"
{
"ok": true,
"reason_code": "summary_loaded",
"correlation_id": "a1b2c3d4-...",
"contact": {
"id": "kotally-contact-uuid",
"external_contact_id": "crm_123",
"ghl_contact_id": "crm_123",
"email": "[email protected]",
"name": "Member Example"
},
"summary": {
"credits_available": 9,
"last_paid_at": "2026-04-16T00:00:00.000Z",
"lifetime_value_cents": 9900,
"payment_events_count": 1,
"entitlements": [
{
"id": "kotally-entitlement-uuid",
"product_config_id": "pc_package_1",
"status": "active",
"credits_remaining": 9,
"expires_at": null
}
],
"recent_ledger": [],
"recent_appointments": [],
"recent_payments": []
}
}

Every endpoint below takes ?location_id= and requires the summary scope. List endpoints also accept ?limit=.

MethodPathReturns
GET/api/v2/contactsMember directory for the location (see List members)
GET/api/v2/contacts/{id}Contact profile
GET/api/v2/contacts/{id}/summaryThe combined snapshot above
GET/api/v2/contacts/{id}/creditsCredit balance and entitlement status
GET/api/v2/contacts/{id}/entitlementsAll entitlements with full detail
GET/api/v2/contacts/{id}/appointments/upcomingUpcoming appointments
GET/api/v2/contacts/{id}/appointments/pastPast appointments
GET/api/v2/contacts/{id}/ledgerCredit ledger entries
GET/api/v2/contacts/{id}/paymentsPayment events
GET/api/v2/contacts/{id}/timelineCombined chronological timeline
reason_codeMeaning
NOT_FOUNDNo contact found for the given id and location_id

See the Overview for HTTP status codes and the full reason-code list.