Customers & Journey
Paginated customer list with conversion data, and full event timeline per customer.
Customers & Journey
PII scope required
Both endpoints on this page return personally identifiable information (names,
emails). Your API key must have the customers:read scope explicitly granted.
Keys without this scope will receive a 403 Forbidden response.
Customer List
GET /api/v1/customersReturns a paginated list of customers who achieved a specific conversion goal within the date range. Each row includes the customer's identity, the conversion details, channel attribution, and lifetime metrics.
Scope: customers:read
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
date_from | string | Yes | Start date in YYYY-MM-DD format. |
date_to | string | Yes | End date in YYYY-MM-DD format. |
goal | string | Yes | Conversion type: payment_received, lead_created, appointment_booked, closed_won, etc. |
search | string | No | Search by customer name or email. |
limit | number | No | Results per page. Default 10, max 100. |
cursor_time | string | No | Pagination cursor timestamp (from previous response). |
cursor_id | string | No | Pagination cursor ID (from previous response). |
Request
curl -H "Authorization: Bearer atb_live_YOUR_KEY" \
"https://www.atribu.app/api/v1/customers?date_from=2026-03-01&date_to=2026-03-25&goal=payment_received&limit=5"const res = await fetch(
"https://www.atribu.app/api/v1/customers?date_from=2026-03-01&date_to=2026-03-25&goal=payment_received&limit=5",
{ headers: { Authorization: "Bearer atb_live_YOUR_KEY" } }
);
const { data, pagination } = await res.json();import requests
res = requests.get(
"https://www.atribu.app/api/v1/customers",
headers={"Authorization": "Bearer atb_live_YOUR_KEY"},
params={
"date_from": "2026-03-01",
"date_to": "2026-03-25",
"goal": "payment_received",
"limit": 5,
},
)
body = res.json()
data = body["data"]
has_next = body["pagination"]["has_next"]Response
{
"data": [
{
"conversion_id": "uuid",
"customer_profile_id": "uuid",
"name": "Jane Smith",
"email": "[email protected]",
"country": "US",
"device": "mobile",
"channel": "Paid Social",
"source": "ig",
"revenue": 299.00,
"revenue_type": "cash",
"conversion_time": "2026-03-22T14:30:00Z",
"time_to_complete_seconds": 172800,
"touch_count": 3,
"touch_channels": ["Paid Social", "Direct"],
"conversion_count": 2,
"total_revenue": 598.00
}
],
"pagination": {
"has_next": true,
"cursor": "2026-03-22T14:30:00Z|uuid"
},
"meta": {
"date_from": "2026-03-01",
"date_to": "2026-03-25",
"profile_id": "uuid"
}
}Response fields
| Field | Type | Description |
|---|---|---|
conversion_id | string | Unique ID of this conversion event. |
customer_profile_id | string | Customer's profile UUID. Use this for the Journey endpoint. |
name | string | Customer's full name (may be null for anonymous visitors). |
email | string | Customer's email address (may be null). |
country | string | ISO country code from the converting session. |
device | string | Device type: desktop, mobile, tablet. |
channel | string | Marketing channel of the last touch before conversion. |
source | string | Traffic source (e.g., ig, fb, google). |
revenue | number | Revenue attributed to this specific conversion. |
revenue_type | string | cash, pipeline, or gross. |
conversion_time | string | ISO 8601 timestamp of the conversion. |
time_to_complete_seconds | number | Seconds between first touch and conversion. |
touch_count | number | Number of marketing touchpoints before conversion. |
touch_channels | string[] | Distinct channels across all touchpoints. |
conversion_count | number | Total conversions by this customer (lifetime). |
total_revenue | number | Total revenue from this customer (lifetime). |
Pagination
This endpoint uses cursor-based pagination. The pagination.cursor field in the
response contains the values needed to fetch the next page.
Fetch the first page with your desired limit.
Check pagination.has_next. If true, split pagination.cursor by | to get
cursor_time and cursor_id.
Pass both values in the next request.
curl -H "Authorization: Bearer atb_live_YOUR_KEY" \
"https://www.atribu.app/api/v1/customers?date_from=2026-03-01&date_to=2026-03-25&goal=payment_received&cursor_time=2026-03-22T14:30:00Z&cursor_id=uuid"let allCustomers = [];
let cursorTime = undefined;
let cursorId = undefined;
while (true) {
const url = new URL("https://www.atribu.app/api/v1/customers");
url.searchParams.set("date_from", "2026-03-01");
url.searchParams.set("date_to", "2026-03-25");
url.searchParams.set("goal", "payment_received");
url.searchParams.set("limit", "50");
if (cursorTime) url.searchParams.set("cursor_time", cursorTime);
if (cursorId) url.searchParams.set("cursor_id", cursorId);
const res = await fetch(url, {
headers: { Authorization: "Bearer atb_live_YOUR_KEY" },
});
const body = await res.json();
allCustomers.push(...body.data);
if (!body.pagination.has_next) break;
[cursorTime, cursorId] = body.pagination.cursor.split("|");
}import requests
all_customers = []
cursor_time = None
cursor_id = None
while True:
params = {
"date_from": "2026-03-01",
"date_to": "2026-03-25",
"goal": "payment_received",
"limit": 50,
}
if cursor_time:
params["cursor_time"] = cursor_time
params["cursor_id"] = cursor_id
body = requests.get(
"https://www.atribu.app/api/v1/customers",
headers={"Authorization": "Bearer atb_live_YOUR_KEY"},
params=params,
).json()
all_customers.extend(body["data"])
if not body["pagination"]["has_next"]:
break
cursor_time, cursor_id = body["pagination"]["cursor"].split("|")Customer Journey
GET /api/v1/customers/{id}/journeyReturns the full event timeline for a single customer -- every page view, form submission, booking, payment, and marketing touchpoint in chronological order.
Scope: customers:read
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Customer profile UUID (path parameter). |
offset | number | No | Skip the first N events. Default 0. |
limit | number | No | Maximum events to return. Default 50, max 100. |
BOLA protection
This endpoint enforces object-level authorization. If the customer ID does not belong to your profile, it returns empty data (not an error). This prevents information disclosure about whether specific customer IDs exist in the system.
Request
curl -H "Authorization: Bearer atb_live_YOUR_KEY" \
"https://www.atribu.app/api/v1/customers/customer-uuid/journey?limit=20"const customerId = "customer-uuid";
const res = await fetch(
`https://www.atribu.app/api/v1/customers/${customerId}/journey?limit=20`,
{ headers: { Authorization: "Bearer atb_live_YOUR_KEY" } }
);
const { data } = await res.json();
console.log(`${data.total_count} events in journey`);import requests
customer_id = "customer-uuid"
res = requests.get(
f"https://www.atribu.app/api/v1/customers/{customer_id}/journey",
headers={"Authorization": "Bearer atb_live_YOUR_KEY"},
params={"limit": 20},
)
events = res.json()["data"]["events"]Response
{
"data": {
"events": [
{
"event_type": "page_view",
"event_name": "page_view",
"event_time": "2026-03-18T10:15:00Z",
"url": "https://example.com/pricing",
"path": "/pricing",
"channel": "Paid Social",
"source": "ig",
"medium": "paid",
"campaign": "Spring Sale",
"value_amount": null,
"currency": null,
"device": "mobile",
"browser": "Safari",
"os": "iOS",
"country": "US",
"city": "New York",
"is_synthetic": false
},
{
"event_type": "payment_received",
"event_name": "payment_received",
"event_time": "2026-03-20T16:45:00Z",
"url": null,
"path": null,
"channel": null,
"source": "stripe",
"medium": null,
"campaign": null,
"value_amount": 299.00,
"currency": "USD",
"device": null,
"browser": null,
"os": null,
"country": null,
"city": null,
"is_synthetic": false
}
],
"total_count": 12
},
"meta": {
"profile_id": "uuid"
}
}Response fields
| Field | Type | Description |
|---|---|---|
event_type | string | Event category (page_view, lead_created, payment_received, etc.). |
event_name | string | Specific event name. |
event_time | string | ISO 8601 timestamp. |
url | string | Full page URL (web events only). |
path | string | URL path component. |
channel | string | Classified marketing channel. |
source | string | Traffic source or payment provider. |
medium | string | Marketing medium (paid, organic, referral, etc.). |
campaign | string | Campaign name (resolved from platform ID). |
value_amount | number | Monetary value (for payment and deal events). |
currency | string | ISO 4217 currency code. |
device | string | Device type. |
browser | string | Browser name. |
os | string | Operating system. |
country | string | ISO country code. |
city | string | City name. |
is_synthetic | boolean | true for off-site conversions that had no website visit. |
total_count | number | Total events in the journey (use with offset/limit for paging). |
Synthetic touchpoints
Events with is_synthetic: true represent off-site conversions (e.g., Meta
lead forms submitted via Instagram that flow into GoHighLevel without a
website visit). Atribu creates synthetic touchpoints for these so they can
still be attributed to the originating ad campaign. See Synthetic Touches for details.