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.
1. Create an API key
Section titled “1. Create an API key”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:
- Sign in to the Kotally admin.
- Open Admin -> Getting Started.
- Choose Use the Kotally API instead (no GHL) if you are not connecting a GoHighLevel sub-account, or complete billing after connecting one.
- After billing is active, open Admin -> API Clients.
To create a key:
- Click New API Client.
- Select the workspace location the key will access.
- Give the client a name (for example, “booking-automation”).
- Choose the scopes the client needs (see the scope table below).
- 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
| Scope | Grants access to |
|---|---|
grant | POST /api/v2/grants |
check | POST /api/v2/entitlements/check-eligibility |
deduct | POST /api/v2/entitlements/deduct |
restore | POST /api/v2/entitlements/restore |
summary | All 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.
2. Authentication
Section titled “2. Authentication”Every request must include the token in the Authorization header:
Authorization: Bearer ktly_<your-token>Content-Type: application/jsonTokens 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.
3. Endpoints
Section titled “3. Endpoints”The base URL for all endpoints is https://app.<your-domain>.
Not using GoHighLevel? Every endpoint below that takes
ghl_contact_idalso acceptsexternal_contact_id— a provider-neutral alias for your own contact identifier. Send either one (they’re interchangeable;external_contact_idwins if both are present). The examples useghl_contact_id, butexternal_contact_idworks identically. The grant creates the contact on first use, so you don’t need to sync contacts beforehand.
POST /api/v2/grants
Section titled “POST /api/v2/grants”Grants credits to a contact after a payment event. Requires the grant scope.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
location_id | string | yes | Must match the token’s location |
request_id | string | yes | Idempotency key — see section 4 |
external_payment_id | string | yes | Your payment reference |
ghl_contact_id | string | yes | The GoHighLevel contact id |
product_config_id | string | yes | The Kotally product config id to apply |
provider | string | no | Defaults to "automation" |
event_type | string | no | Defaults to "payment_confirmed" |
amount_cents | integer | no | Payment amount in cents (non-negative) |
currency | string | no | e.g. "USD" |
paid_at | string | no | ISO 8601 timestamp |
email | string | no | Contact email hint |
name | string | no | Contact name hint |
external_ref | string | no | Additional reference for replay lookup |
metadata | object | no | Arbitrary key/value pairs stored with the grant |
Example request
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_code | Meaning |
|---|---|
duplicate_payment_event | The external_payment_id was already processed |
BILLING_SUSPENDED | Workspace billing is suspended |
REQUEST_IN_PROGRESS | Same 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
| Field | Type | Required | Notes |
|---|---|---|---|
location_id | string | yes | Must match the token’s location |
ghl_contact_id | string | yes | The GoHighLevel contact id |
product_config_id | string | yes* | The product config to check against |
calendar_id | string | yes* | Alternative to product_config_id |
amount | integer | no | Credits to check; defaults to 1 |
* Either product_config_id or calendar_id is required.
Example request
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_code | Meaning |
|---|---|
NO_ENTITLEMENT | Contact has no matching entitlement |
INSUFFICIENT_CREDITS | Contact does not have enough credits |
POST /api/v2/entitlements/deduct
Section titled “POST /api/v2/entitlements/deduct”Deducts credits from a contact’s entitlement. Requires the deduct scope.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
location_id | string | yes | Must match the token’s location |
request_id | string | yes | Idempotency key — see section 4 |
ghl_contact_id | string | yes | The GoHighLevel contact id |
product_config_id | string | yes* | The product config to deduct from |
calendar_id | string | yes* | Alternative to product_config_id |
amount | integer | no | Credits to deduct; defaults to 1 |
external_ref | string | no | Booking or event reference |
appointment_time | string | no | ISO 8601 timestamp of the appointment |
* Either product_config_id or calendar_id is required.
Example request
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_code | Meaning |
|---|---|
NO_ENTITLEMENT | Contact has no matching entitlement |
INSUFFICIENT_CREDITS | Not enough credits to complete the deduction |
BILLING_SUSPENDED | Workspace billing is suspended |
REQUEST_IN_PROGRESS | Same request_id is still being processed — retry after a short delay |
POST /api/v2/entitlements/restore
Section titled “POST /api/v2/entitlements/restore”Restores credits to a contact’s entitlement, typically on cancellation. Requires the restore scope. Subject to the workspace cancellation window setting.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
location_id | string | yes | Must match the token’s location |
request_id | string | yes | Idempotency key — see section 4 |
ghl_contact_id | string | yes | The GoHighLevel contact id |
product_config_id | string | yes* | The product config to restore into |
calendar_id | string | yes* | Alternative to product_config_id |
amount | integer | no | Credits to restore; defaults to 1 |
external_ref | string | no | Booking or event reference |
appointment_time | string | no | ISO 8601 timestamp of the cancelled appointment (used for window check) |
* Either product_config_id or calendar_id is required.
Example request
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_code | Meaning |
|---|---|
NO_ENTITLEMENT | Contact has no matching entitlement |
CANCELLATION_WINDOW_EXPIRED | The appointment is outside the allowed cancellation window |
BILLING_SUSPENDED | Workspace billing is suspended |
REQUEST_IN_PROGRESS | Same 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
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", "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_code | Meaning |
|---|---|
NOT_FOUND | No contact found for the given ghl_contact_id and location_id |
Other contact read endpoints
Section titled “Other contact read endpoints”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).
| Method | Path | Returns |
|---|---|---|
GET | /api/v2/contacts/:id | Contact profile |
GET | /api/v2/contacts/:id/credits | Credit balance and entitlement status |
GET | /api/v2/contacts/:id/entitlements | All entitlements with full detail |
GET | /api/v2/contacts/:id/appointments/upcoming | Upcoming appointments |
GET | /api/v2/contacts/:id/appointments/past | Past appointments |
GET | /api/v2/contacts/:id/ledger | Credit ledger entries |
GET | /api/v2/contacts/:id/payments | Payment events |
GET | /api/v2/contacts/:id/timeline | Combined chronological timeline |
4. Idempotency
Section titled “4. Idempotency”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
-deductfor 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_idis 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>-deductbooking_<booking-id>-restoreDo not reuse a request_id across different operations or different contacts.
5. Errors
Section titled “5. Errors”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
| Status | When |
|---|---|
| 200 | Business-logic outcome (including ineligible/insufficient — check ok) |
| 400 | Validation error (missing required field, wrong type) |
| 401 | Missing or invalid bearer token, wrong scope, wrong location |
| 409 | REQUEST_IN_PROGRESS — a duplicate in-flight request |
| 429 | RATE_LIMITED — slow down and retry |
Common reason codes
reason_code | Meaning |
|---|---|
UNAUTHORIZED | Missing bearer token, invalid token, insufficient scope, or wrong location |
RATE_LIMITED | Too many requests from this API client |
VALIDATION_ERROR | A required field is missing or has an invalid value |
NO_ENTITLEMENT | The contact has no entitlement matching the given product config or calendar |
INSUFFICIENT_CREDITS | The contact’s entitlement does not have enough credits |
CANCELLATION_WINDOW_EXPIRED | The restore was rejected because the appointment is past the cancellation window |
BILLING_SUSPENDED | The workspace billing is suspended; credit mutations are paused |
duplicate_payment_event | A grant was skipped because the external_payment_id was already processed |
REQUEST_IN_PROGRESS | The same request_id is being processed by a concurrent request |
NOT_FOUND | The requested contact does not exist for this location |