Grant credits
POST
/api/v2/grants Grants credits to a contact after a payment event. Call this when a purchase is confirmed in your own checkout. Requires the grant scope. Uses the common headers and is idempotent via request_id.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
location_id | string | yes | Must match the token’s location |
request_id | string | yes | Idempotency key |
external_payment_id | string | yes | Your payment reference; deduplicates grants |
external_contact_id | string | yes* | Provider-neutral contact id for non-GHL systems. Send this or ghl_contact_id — external_contact_id wins if both are present |
ghl_contact_id | string | yes* | The GoHighLevel contact id. Interchangeable with external_contact_id |
product_config_id | string | yes | The Kotally product config 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 |
* Exactly one of external_contact_id or ghl_contact_id is required. The grant creates the contact if it doesn’t exist yet, so non-GHL systems don’t need to sync contacts first.
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" }'const res = await fetch("https://app.<your-domain>/api/v2/grants", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOTALLY_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ 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", }),});const data = await res.json();// data.ok, data.credits_granted, data.balance_afterResponse (200)
Section titled “Response (200)”{ "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}credits_granted— derived from the product config, not the payment amount.balance_after— the contact’s total available credits after the grant.- Re-sending the same
external_payment_idreturnsduplicate_payment_eventwithout granting again.
Failure reason codes
Section titled “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 |
See the Overview for HTTP status codes and the full reason-code list.