Automation API
The Automation API records signups, purchases, and refunds, manages Offers, and sends new records to hooks. An automation platform such as Zapier calls it for a creator. The creator grants access to one Workspace through OAuth 2.0.
Base URL: https://app.channelgraph.io/api/v1/automation
Authorization
Section titled “Authorization”The API uses the OAuth 2.0 authorization code grant with PKCE.
| Item | Value |
|---|---|
| Authorization endpoint | https://app.channelgraph.io/oauth/authorize |
| Token endpoint | https://app.channelgraph.io/oauth/token |
| Revocation endpoint | https://app.channelgraph.io/oauth/revoke |
| Server metadata | https://app.channelgraph.io/.well-known/oauth-authorization-server |
| Scope | automation |
| Resource indicator | https://app.channelgraph.io/api/v1/automation |
| PKCE | Required. Use S256. The server refuses plain. |
| Client type | Confidential. The client authenticates with its secret. |
| Grant types | authorization_code and refresh_token |
| Access token lifetime | One year |
ChannelGraph registers each client and issues its client ID and secret. A client registers each redirect URI with ChannelGraph first.
1. Send the creator to the authorization endpoint
Section titled “1. Send the creator to the authorization endpoint”GET https://app.channelgraph.io/oauth/authorize ?response_type=code &client_id=<client id> &redirect_uri=<registered redirect URI> &scope=automation &state=<state> &code_challenge=<S256 challenge> &code_challenge_method=S256 &resource=https://app.channelgraph.io/api/v1/automationThe creator signs in and selects one of their Workspaces on the consent screen.
The grant covers that Workspace only.
Then ChannelGraph redirects to redirect_uri with code and state.
Send the resource parameter exactly once with the exact value above.
A missing or different value fails with the invalid_target error.
2. Exchange the code for tokens
Section titled “2. Exchange the code for tokens”Send a form-encoded POST to the token endpoint.
Authenticate the client with HTTP Basic, or send client_id and client_secret in the body.
curl https://app.channelgraph.io/oauth/token \ -d grant_type=authorization_code \ -d code=<code> \ -d redirect_uri=<registered redirect URI> \ -d code_verifier=<PKCE verifier> \ -d client_id=<client id> \ -d client_secret=<client secret> \ -d resource=https://app.channelgraph.io/api/v1/automation{ "access_token": "<access token>", "expires_in": 31536000, "token_type": "Bearer", "scope": "automation", "refresh_token": "<refresh token>"}3. Refresh the access token
Section titled “3. Refresh the access token”curl https://app.channelgraph.io/oauth/token \ -d grant_type=refresh_token \ -d refresh_token=<refresh token> \ -d client_id=<client id> \ -d client_secret=<client secret> \ -d resource=https://app.channelgraph.io/api/v1/automationThe refresh token does not rotate. Thus several runs can refresh one connection at the same time.
4. Call the API
Section titled “4. Call the API”Send the access token in the Authorization header.
curl https://app.channelgraph.io/api/v1/automation/me \ -H "Authorization: Bearer <access token>"The token names the Workspace, so no route takes a Workspace ID. A token stops working when the creator leaves the Workspace or when someone deletes the Workspace.
Conventions
Section titled “Conventions”- Each request and response body is JSON.
- Money is a decimal string in major units, such as
"49.99". The number of decimal places follows the currency, such as"4900"for JPY. - A time is an ISO 8601 string with an offset, such as
"2026-10-08T18:24:05+00:00". - A record ID is an opaque string.
- A response can gain new fields. Ignore a field that you do not know.
Errors
Section titled “Errors”An error body has a detail field.
{ "detail": "offer_id: no offer in this workspace has this id." }| Status | Meaning |
|---|---|
401 |
The token is missing, expired, revoked, or for another resource. The body is {"detail": "Unauthorized"}. |
404 |
The record does not exist. |
409 |
The record conflicts with another one, or the Workspace has reached a limit. A limit error has "code": "limit_reached". |
413 |
The body is larger than 16 KiB. |
422 |
The request is not valid. detail names the field. When the body does not match the schema, detail is a list of field errors. |
429 |
The connection made too many calls. The Retry-After header gives the wait in seconds. |
Rate limit
Section titled “Rate limit”Each client can make 600 calls a minute to one Workspace.
The count resets at the start of each minute.
Past the limit, a call fails with 429, and Retry-After states the wait.
{ "detail": "Too many attempts. Try again in a minute." }Objects
Section titled “Objects”Conversion
Section titled “Conversion”A signup or a purchase of an Offer.
| Field | Type | Meaning |
|---|---|---|
id |
string | The conversion ID. |
kind |
string | signup or purchase. |
ts |
string | When the signup or purchase happened. |
created_at |
string | When ChannelGraph recorded it. |
source |
string | Where it came from: ingress for this API and event ingress, snippet for the site snippet, or an Integration such as gumroad or kit. |
offer |
object | The Offer, as {"id", "name"}. |
amount |
string or null | What the purchase counts after refunds. Null on a signup, and on a purchase with no amount and no Offer price. |
amount_refunded |
string or null | The part that refunds took back. Null when amount is null. |
currency |
string | The currency code of both amounts. Empty when there is no amount. |
person |
Person or null | The Person who converted. Null when ChannelGraph knows no Person for it. |
credited_content |
array of ContentRef | Each piece of Content that earned credit for the conversion. |
Person
Section titled “Person”One human that ChannelGraph knows in the Workspace.
| Field | Type | Meaning |
|---|---|---|
id |
string | The Person ID. |
email |
string | Their email address. |
created_at |
string | When ChannelGraph first saw them. |
purchases |
integer | How many purchases they made. |
spent |
string or null | What they spent after refunds, in the Workspace’s display currency. |
spent_currency |
string | The Workspace’s display currency. |
found_through |
FoundThrough or null | How they first found the creator. Null when ChannelGraph knows no first event. |
FoundThrough
Section titled “FoundThrough”The first thing a Person did.
| Field | Type | Meaning |
|---|---|---|
kind |
string | content for a click on a link on Content, link for a click on a Freestanding Link, or source for an event that no click explains. |
ts |
string | When it happened. |
touchpoint_kind |
string | The kind of the first event, such as click or signup. |
content |
ContentRef or null | The Content. Null unless kind is content. |
link_label |
string | The label of the Freestanding Link. Empty unless kind is link. |
source |
string | The system that reported the event, such as kit. Empty unless kind is source. |
ContentRef
Section titled “ContentRef”| Field | Type | Meaning |
|---|---|---|
id |
string | The Content ID. |
title |
string | The title. |
platform |
string | The platform of its Channel, such as youtube. Empty when it has no Channel. |
url |
string | Its address on the platform. |
| Field | Type | Meaning |
|---|---|---|
id |
string | The Offer ID. |
name |
string | The name. |
type |
string | product, service, lead_magnet, or newsletter. |
price |
string or null | The price. Null for a free Offer. |
currency |
string | The currency of the price. Empty for a free Offer. |
created_at |
string | When the Offer was created. |
Experiment
Section titled “Experiment”An A/B test of two destinations on one Link.
| Field | Type | Meaning |
|---|---|---|
id |
string | The A/B test ID. |
started_at |
string | When it started. |
ended_at |
string or null | When it found its winner. |
link |
object | The Link under test, as {"id", "label", "url"}. url is the tracked address. |
content |
ContentRef or null | The Content that the Link sits on. Null for a Freestanding Link. |
winner |
object or null | The winning destination, as {"label", "url"}. |
lift |
number or null | The winner’s relative gain over the other destination. 0.18 means 18% better. |
Example Conversion
Section titled “Example Conversion”{ "id": "7hq2m9x4c1v8d3ka", "kind": "purchase", "ts": "2026-10-08T18:24:05+00:00", "created_at": "2026-10-08T18:24:07+00:00", "source": "ingress", "offer": { "id": "p4c6e8g0j2l4n6q8", "name": "Video Planning Course" }, "amount": "49.00", "amount_refunded": "0.00", "currency": "USD", "person": { "id": "m1b3d5f7h9k1m3p5", "email": "jordan@example.com", "created_at": "2026-09-14T16:02:11+00:00", "purchases": 2, "spent": "98.00", "spent_currency": "USD", "found_through": { "kind": "content", "ts": "2026-09-14T15:58:40+00:00", "touchpoint_kind": "click", "content": { "id": "k8v2q4m6t8w0y2a4", "title": "How I Plan a Week of Videos", "platform": "youtube", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }, "link_label": "", "source": "" } }, "credited_content": [ { "id": "k8v2q4m6t8w0y2a4", "title": "How I Plan a Week of Videos", "platform": "youtube", "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" } ]}Example Experiment
Section titled “Example Experiment”{ "id": "t0f2h4k6m8p0r2t4", "started_at": "2026-09-20T09:00:00+00:00", "ended_at": "2026-10-07T21:30:00+00:00", "link": { "id": "v3g5j7l9n1q3", "label": "Course link", "url": "https://go.example.com/course" }, "content": null, "winner": { "label": "Sales page B", "url": "https://example.com/course-b" }, "lift": 0.18}Routes
Section titled “Routes”Get the connection
Section titled “Get the connection”GET /me
Returns the Workspace and the creator that the token names.
| Field | Type | Meaning |
|---|---|---|
workspace_id |
string | The Workspace ID. |
workspace_name |
string | The Workspace name. |
user_email |
string | The email address of the creator who granted access. |
{ "workspace_id": "w5n7q9s1u3w5y7a9", "workspace_name": "Jordan Makes Videos", "user_email": "jordan@example.com"}Status codes: 200, 401, 429.
Record an event
Section titled “Record an event”POST /events
Records a signup, a purchase, or a refund.
The body is an event with the same fields and rules as event ingress.
This route takes signup, purchase, and refund.
It refuses identify.
curl https://app.channelgraph.io/api/v1/automation/events \ -H "Authorization: Bearer <access token>" \ -H "Content-Type: application/json" \ -d '{ "type": "purchase", "offer_id": "p4c6e8g0j2l4n6q8", "order_id": "1001", "amount": "49.00", "currency": "USD", "email": "jordan@example.com", "token": "cv_Xq3v9LmA0bC1dE2fG3hI4j" }'Response 202:
| Field | Type | Meaning |
|---|---|---|
event_id |
string | The event ID. It is the event_id you sent, or the ID that ChannelGraph derived. |
conversion |
Conversion or null | The conversion that the event made or changed. For a refund, this is the refunded purchase. Null when the event made no conversion. |
{ "event_id": "purchase:order:p4c6e8g0j2l4n6q8:1001", "conversion": { "id": "7hq2m9x4c1v8d3ka", "kind": "purchase", "...": "..." }}A retry of the same event answers 202 with the same event_id and records nothing new.
See idempotency.
Status codes: 202, 401, 413, 422, 429.
List Offers
Section titled “List Offers”GET /offers
Returns the Workspace’s Offers, newest first.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
name |
string | Return only Offers with this exact name. Case does not matter. | |
q |
string | Return only Offers whose name contains this text. Case does not matter. | |
limit |
integer | 100 |
How many Offers to return, from 1 to 100. |
offset |
integer | 0 |
How many Offers to skip, from 0 to 10,000. |
The server moves an out-of-range limit or offset to the nearest allowed value.
curl "https://app.channelgraph.io/api/v1/automation/offers?name=Video%20Planning%20Course&limit=1" \ -H "Authorization: Bearer <access token>"{ "items": [ { "id": "p4c6e8g0j2l4n6q8", "name": "Video Planning Course", "type": "product", "price": "49.00", "currency": "USD", "created_at": "2026-08-02T10:15:00+00:00" } ]}Status codes: 200, 401, 422, 429.
Create an Offer
Section titled “Create an Offer”POST /offers
| Field | Type | Required | Meaning |
|---|---|---|---|
name |
string | Yes | The name, 1 to 255 characters. |
type |
string | No | product, service, lead_magnet, or newsletter. The default is product. |
price |
string or number | No | The price in major units, such as "49.00". Omit it, or send null or "", for a free Offer. |
currency |
string | No | The three-letter currency code. A price above zero needs one. |
description |
string | No | Up to 2,000 characters. |
curl https://app.channelgraph.io/api/v1/automation/offers \ -H "Authorization: Bearer <access token>" \ -H "Content-Type: application/json" \ -d '{"name": "Video Planning Course", "type": "product", "price": "49.00", "currency": "USD"}'Response 201: the new Offer.
When an Offer that you created has the name, the response is 409 with that Offer:
{ "detail": "An offer with that name already exists.", "offer": { "id": "p4c6e8g0j2l4n6q8", "name": "Video Planning Course", "...": "..." }}A Workspace holds up to 10,000 Offers.
Past that, the response is 409 with "code": "limit_reached".
Status codes: 201, 401, 409, 422, 429.
Find a Person
Section titled “Find a Person”GET /people/find
| Parameter | Type | Required | Meaning |
|---|---|---|---|
email |
string | Yes | The email address. Case does not matter. |
Returns {"items": [...]} with the matching Person, or an empty list.
curl "https://app.channelgraph.io/api/v1/automation/people/find?email=jordan@example.com" \ -H "Authorization: Bearer <access token>"Status codes: 200, 401, 422, 429.
Recent records
Section titled “Recent records”These routes return the newest records of one kind. A platform uses them to show samples while a creator sets up a trigger.
| Route | Returns |
|---|---|
GET /conversions |
The newest conversions, as {"items": [Conversion]}. |
GET /people |
The newest People with an email address, as {"items": [Person]}. |
GET /experiments |
The newest A/B tests that found a winner, as {"items": [Experiment]}. |
| Parameter | Type | Default | Meaning |
|---|---|---|---|
limit |
integer | 3 |
How many records to return, from 1 to 10. |
Status codes: 200, 401, 422, 429.
A hook sends each new record of one kind to a target URL. A platform makes a hook when a creator turns on an instant trigger, and deletes it when the trigger goes off.
| Event | Sends |
|---|---|
conversion.created |
Each new Conversion. |
person.created |
Each Person who gives an email address for the first time. |
experiment.decided |
Each A/B test that finds its winner. An A/B test that the creator ends does not count. |
Subscribe
Section titled “Subscribe”POST /hooks
| Field | Type | Required | Meaning |
|---|---|---|---|
event |
string | Yes | One of the events above. |
target_url |
string | Yes | Where to send each record, up to 2,000 characters. It must start with https://hooks.zapier.com/. |
curl https://app.channelgraph.io/api/v1/automation/hooks \ -H "Authorization: Bearer <access token>" \ -H "Content-Type: application/json" \ -d '{"event": "conversion.created", "target_url": "https://hooks.zapier.com/hooks/standard/123/abc/"}'Response 201:
| Field | Type | Meaning |
|---|---|---|
id |
string | The hook ID. Keep it to unsubscribe. |
event |
string | The event. |
target_url |
string | The target URL. |
created_at |
string | The hook’s creation time. |
A Workspace holds up to 500 hooks.
Past that, the response is 409 with "code": "limit_reached".
Status codes: 201, 401, 409, 422, 429.
Unsubscribe
Section titled “Unsubscribe”DELETE /hooks/{hook_id}
Deletes the hook. Only the client that made a hook can delete it.
Status codes: 204, 401, 404, 429.
A 404 also means that ChannelGraph already removed the hook.
Delivery
Section titled “Delivery”ChannelGraph sends each record as one JSON object in one POST to the target URL. The body is a Conversion, a Person, or an Experiment.
- A record goes to each hook at most once.
- A record waits about 30 seconds before it goes. The wait lets ChannelGraph find its Person and its credited Content.
- A hook gets only the records that arrive after the platform makes the hook.
- A record whose own time is more than one day old never goes. Thus history that an Integration brings in sends nothing.
- A
2xxanswer counts as delivered. - After any other answer, a timeout, or a network error, ChannelGraph tries again. The wait between tries doubles from one minute up to 15 minutes. The tries stop one hour after the record arrived.
- A
410answer deletes the hook. - ChannelGraph does not follow a redirect. A request times out after 10 seconds.
- When the creator who made the hook leaves the Workspace, ChannelGraph deletes the hook.