Skip to content

API Quickstart

The Kotally machine API lives at https://app.<your-domain>/api/v2. It lets external automations grant credits after a payment, check and deduct credits at booking time, restore credits on cancellation, and read contact entitlement data — all without going through GoHighLevel.

This guide walks through everything a standalone integration needs. For the full per-endpoint reference — request fields, responses, and reason codes — see the API reference.

API keys are managed under Admin -> API Clients.

Before you can reach that page your workspace must have billing set up. If you have not done that yet:

  1. Sign in to the Kotally admin.
  2. Open Admin -> Getting Started.
  3. Choose Use the Kotally API instead (no GHL) if you are not connecting a GoHighLevel sub-account, or complete billing after connecting one.
  4. After billing is active, open Admin -> API Clients.

To create a key:

  1. Click New API Client.
  2. Select the workspace location the key will access.
  3. Give the client a name (for example, “booking-automation”).
  4. Choose the scopes the client needs (see the scope table below).
  5. Click Create. The raw token is shown once — copy it immediately and store it in a secret manager. The token format is ktly_<uuid>_<uuid>.

Scopes

ScopeGrants access to
grantPOST /api/v2/grants
checkPOST /api/v2/entitlements/check-eligibility
deductPOST /api/v2/entitlements/deduct
restorePOST /api/v2/entitlements/restore
summaryAll GET /api/v2/contacts/… read endpoints

Grant the minimum set of scopes the client actually needs. You can rotate a token or deactivate a client from the same page if it is ever compromised.

Every request must include the token in the Authorization header:

Authorization: Bearer ktly_<your-token>
Content-Type: application/json

Tokens are per-location and scope-limited. The location_id you include in request bodies must match the location the API client was created for — requests for a different location are rejected with UNAUTHORIZED.

The base URL for all endpoints is https://app.<your-domain>.

Not using GoHighLevel? Every endpoint below that takes ghl_contact_id also accepts external_contact_id — a provider-neutral alias for your own contact identifier. Send either one (they’re interchangeable; external_contact_id wins if both are present). The examples use ghl_contact_id, but external_contact_id works identically. The grant creates the contact on first use, so you don’t need to sync contacts beforehand.


Grants credits to a contact after a payment event. Requires the grant scope.

Request body

FieldTypeRequiredNotes
location_idstringyesMust match the token’s location
request_idstringyesIdempotency key — see section 4
external_payment_idstringyesYour payment reference
ghl_contact_idstringyesThe GoHighLevel contact id
product_config_idstringyesThe Kotally product config id to apply
providerstringnoDefaults to "automation"
event_typestringnoDefaults to "payment_confirmed"
amount_centsintegernoPayment amount in cents (non-negative)
currencystringnoe.g. "USD"
paid_atstringnoISO 8601 timestamp
emailstringnoContact email hint
namestringnoContact name hint
external_refstringnoAdditional reference for replay lookup
metadataobjectnoArbitrary key/value pairs stored with the grant

Example request

Terminal window
curl -X POST https://app.<your-domain>/api/v2/grants \
-H "Authorization: Bearer ktly_<your-token>" \
-H "Content-Type: application/json" \
-d '{
"location_id": "loc_1",
"request_id": "grant_req_123",
"external_payment_id": "payment_123",
"ghl_contact_id": "ghl_contact_123",
"product_config_id": "pc_package_1",
"amount_cents": 9900,
"currency": "USD",
"paid_at": "2026-04-16T00:00:00.000Z",
"provider": "manual",
"event_type": "payment_confirmed",
"email": "[email protected]",
"name": "Member Example"
}'

Success response

{
"ok": true,
"reason_code": "grant_applied",
"correlation_id": "a1b2c3d4-...",
"location_id": "loc_1",
"contact_id": "kotally-contact-uuid",
"entitlement_id": "kotally-entitlement-uuid",
"credits_granted": 10,
"balance_after": 10
}

Failure reason codes

reason_codeMeaning
duplicate_payment_eventThe external_payment_id was already processed
BILLING_SUSPENDEDWorkspace billing is suspended
REQUEST_IN_PROGRESSSame request_id is still being processed — retry after a short delay

POST /api/v2/entitlements/check-eligibility

Section titled “POST /api/v2/entitlements/check-eligibility”

Checks whether a contact has enough credits to proceed. Does not modify any balance. Requires the check scope.

Request body

FieldTypeRequiredNotes
location_idstringyesMust match the token’s location
ghl_contact_idstringyesThe GoHighLevel contact id
product_config_idstringyes*The product config to check against
calendar_idstringyes*Alternative to product_config_id
amountintegernoCredits to check; defaults to 1

* Either product_config_id or calendar_id is required.

Example request

Terminal window
curl -X POST https://app.<your-domain>/api/v2/entitlements/check-eligibility \
-H "Authorization: Bearer ktly_<your-token>" \
-H "Content-Type: application/json" \
-d '{
"location_id": "loc_1",
"ghl_contact_id": "ghl_contact_123",
"product_config_id": "pc_package_1",
"amount": 1
}'

Success response

{
"ok": true,
"reason_code": "eligible",
"correlation_id": "a1b2c3d4-...",
"balance_after": 9
}

Failure reason codes

reason_codeMeaning
NO_ENTITLEMENTContact has no matching entitlement
INSUFFICIENT_CREDITSContact does not have enough credits

Deducts credits from a contact’s entitlement. Requires the deduct scope.

Request body

FieldTypeRequiredNotes
location_idstringyesMust match the token’s location
request_idstringyesIdempotency key — see section 4
ghl_contact_idstringyesThe GoHighLevel contact id
product_config_idstringyes*The product config to deduct from
calendar_idstringyes*Alternative to product_config_id
amountintegernoCredits to deduct; defaults to 1
external_refstringnoBooking or event reference
appointment_timestringnoISO 8601 timestamp of the appointment

* Either product_config_id or calendar_id is required.

Example request

Terminal window
curl -X POST https://app.<your-domain>/api/v2/entitlements/deduct \
-H "Authorization: Bearer ktly_<your-token>" \
-H "Content-Type: application/json" \
-d '{
"location_id": "loc_1",
"request_id": "booking-123-deduct",
"ghl_contact_id": "ghl_contact_123",
"product_config_id": "pc_package_1",
"amount": 1,
"external_ref": "booking_123"
}'

Success response

{
"ok": true,
"reason_code": "deducted",
"correlation_id": "a1b2c3d4-...",
"balance_after": 9,
"entitlement_id": "kotally-entitlement-uuid"
}

Failure reason codes

reason_codeMeaning
NO_ENTITLEMENTContact has no matching entitlement
INSUFFICIENT_CREDITSNot enough credits to complete the deduction
BILLING_SUSPENDEDWorkspace billing is suspended
REQUEST_IN_PROGRESSSame request_id is still being processed — retry after a short delay

Restores credits to a contact’s entitlement, typically on cancellation. Requires the restore scope. Subject to the workspace cancellation window setting.

Request body

FieldTypeRequiredNotes
location_idstringyesMust match the token’s location
request_idstringyesIdempotency key — see section 4
ghl_contact_idstringyesThe GoHighLevel contact id
product_config_idstringyes*The product config to restore into
calendar_idstringyes*Alternative to product_config_id
amountintegernoCredits to restore; defaults to 1
external_refstringnoBooking or event reference
appointment_timestringnoISO 8601 timestamp of the cancelled appointment (used for window check)

* Either product_config_id or calendar_id is required.

Example request

Terminal window
curl -X POST https://app.<your-domain>/api/v2/entitlements/restore \
-H "Authorization: Bearer ktly_<your-token>" \
-H "Content-Type: application/json" \
-d '{
"location_id": "loc_1",
"request_id": "booking-123-restore",
"ghl_contact_id": "ghl_contact_123",
"product_config_id": "pc_package_1",
"amount": 1,
"external_ref": "booking_123",
"appointment_time": "2026-05-01T10:00:00.000Z"
}'

Success response

{
"ok": true,
"reason_code": "restored",
"correlation_id": "a1b2c3d4-...",
"balance_after": 10,
"entitlement_id": "kotally-entitlement-uuid"
}

Failure reason codes

reason_codeMeaning
NO_ENTITLEMENTContact has no matching entitlement
CANCELLATION_WINDOW_EXPIREDThe appointment is outside the allowed cancellation window
BILLING_SUSPENDEDWorkspace billing is suspended
REQUEST_IN_PROGRESSSame request_id is still being processed — retry after a short delay

GET /api/v2/contacts/:ghl_contact_id/summary

Section titled “GET /api/v2/contacts/:ghl_contact_id/summary”

Returns a snapshot of a contact’s credits, entitlements, recent ledger, recent appointments, and recent payments. Requires the summary scope.

location_id is passed as a query parameter, not in the body.

Example request

Terminal window
curl "https://app.<your-domain>/api/v2/contacts/ghl_contact_123/summary?location_id=loc_1" \
-H "Authorization: Bearer ktly_<your-token>"

Success response

{
"ok": true,
"reason_code": "summary_loaded",
"correlation_id": "a1b2c3d4-...",
"contact": {
"id": "kotally-contact-uuid",
"ghl_contact_id": "ghl_contact_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": []
}
}

Failure reason codes

reason_codeMeaning
NOT_FOUNDNo contact found for the given ghl_contact_id and location_id

All of the following also require the summary scope and accept ?location_id= as a query parameter. List endpoints accept an optional ?limit= parameter (default 20, max 100).

MethodPathReturns
GET/api/v2/contacts/:idContact profile
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

The state-changing endpoints — POST /api/v2/grants, POST /api/v2/entitlements/deduct, and POST /api/v2/entitlements/restore — all require a request_id field.

How it works:

  • Choose a stable, unique string for each logical operation (for example, your internal payment ID for a grant, or your booking ID suffixed with -deduct for a deduction).
  • If your request times out or you receive a network error, resend the exact same payload with the same request_id. Kotally will return the original result without performing the operation again.
  • If the same request_id is received while the first request is still processing, Kotally returns {"ok": false, "reason_code": "REQUEST_IN_PROGRESS", "retryable": true} with HTTP 409. Wait a moment and retry.

Good request_id values:

grant_req_<payment-id>
booking_<booking-id>-deduct
booking_<booking-id>-restore

Do not reuse a request_id across different operations or different contacts.

All responses include ok, reason_code, and (for non-check endpoints) correlation_id. The correlation_id is unique per response — include it in support requests.

Error response shape

{
"ok": false,
"reason_code": "INSUFFICIENT_CREDITS",
"correlation_id": "a1b2c3d4-...",
"message": "Optional human-readable detail"
}

HTTP status codes

StatusWhen
200Business-logic outcome (including ineligible/insufficient — check ok)
400Validation error (missing required field, wrong type)
401Missing or invalid bearer token, wrong scope, wrong location
409REQUEST_IN_PROGRESS — a duplicate in-flight request
429RATE_LIMITED — slow down and retry

Common reason codes

reason_codeMeaning
UNAUTHORIZEDMissing bearer token, invalid token, insufficient scope, or wrong location
RATE_LIMITEDToo many requests from this API client
VALIDATION_ERRORA required field is missing or has an invalid value
NO_ENTITLEMENTThe contact has no entitlement matching the given product config or calendar
INSUFFICIENT_CREDITSThe contact’s entitlement does not have enough credits
CANCELLATION_WINDOW_EXPIREDThe restore was rejected because the appointment is past the cancellation window
BILLING_SUSPENDEDThe workspace billing is suspended; credit mutations are paused
duplicate_payment_eventA grant was skipped because the external_payment_id was already processed
REQUEST_IN_PROGRESSThe same request_id is being processed by a concurrent request
NOT_FOUNDThe requested contact does not exist for this location