# Attriboo API documentation

Base URL: https://www.attriboo.com/api/v1

Examples use example IDs and illustrative responses. Replace them with your own returned website IDs and recorded activity.

## Make your first API request

Find your key, choose a website and read your first revenue report.

### 1. Configure a private credential

Sign in to Attriboo and open Settings → API & MCP. Workspace owners and admins can copy the workspace key. Set ATTRIBOO_TOKEN in your server's environment or local secret manager before running these examples. Do not paste a real key into this page, client-side JavaScript or a public repository.

The cURL examples use Bash environment-variable syntax. On Windows, use Bash or adapt variables to your shell. A connected app can use an approved OAuth access token instead of a workspace key for reports.

[Choose authentication and permissions](https://www.attriboo.com/developers/authentication)

### 2. List your websites

Use GET /sites to find the real ID, domain and currency. Replace site_example in the following examples with a returned ID. The API does not return public demo numbers for your account.

```bash
curl "https://www.attriboo.com/api/v1/sites" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### 3. Read a revenue overview

This example requests October 1–7, 2026, inclusive, without a comparison. Choose dates containing your own recorded activity. Read revenue in cents: 20000 is 200.00 in the website's currency.

```bash
curl "https://www.attriboo.com/api/v1/stats/overview?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### 4. Call the API from your server

This Node.js example checks the HTTP result and the response envelope before reading metrics. Run it on the server with ATTRIBOO_TOKEN and ATTRIBOO_SITE configured privately. An empty report on a new installation is not a tracking verification.

```javascript
const url = new URL("https://www.attriboo.com/api/v1/stats/overview");
url.search = new URLSearchParams({
  site: process.env.ATTRIBOO_SITE, period: "7d", compare: "none"
});
const response = await fetch(url, {
  headers: { Authorization: "Bearer " + process.env.ATTRIBOO_TOKEN }
});
const result = await response.json();
if (!response.ok || result.status !== "success") {
  throw new Error(result.error?.message || "Request failed");
}
console.log(result.data.metrics.revenue / 100, result.data.site.currency);
```

[Overview response fields](https://www.attriboo.com/developers/overview)

[Use the official CLI](https://www.attriboo.com/docs/cli)

## Authentication and permissions

Choose a workspace key or OAuth grant for the operation you need.

### Send credentials in a header

Authenticated REST endpoints accept Authorization: Bearer TOKEN. Workspace keys can also use x-attriboo-key. Do not put credentials in query strings. List websites first so every request uses the intended workspace and site.

Workspace keys begin atb_live_. OAuth access tokens begin atb_oat_. The shared workspace key can read all workspace reports and make supported event/payment writes. There are no per-website or read-only workspace API keys.

```text
Authorization: Bearer $ATTRIBOO_TOKEN
```

### Choose the least access needed

OAuth approval is for a selected workspace. The account holder completes sign-in and approval. Optional website setup requires a new approval for analytics:read sites:write and current workspace owner/admin access. Refreshing an existing report-only grant does not add setup access.

| Credential | Read reports | Website setup | Events, payments, crawls |
| --- | --- | --- | --- |
| Workspace key | Yes | No | Yes |
| OAuth analytics:read | Yes | No | No |
| OAuth analytics:read sites:write | Yes | Owner/admin only | No |
| No credential | No | No | Only the public tracker routes described separately |

[Build an OAuth connection](https://www.attriboo.com/developers/oauth)

[Connect an AI assistant through MCP](https://www.attriboo.com/docs/mcp)

### Rotate or disconnect access

Regenerate a workspace key in Settings → API & MCP. The old key stops working immediately; update every server, plugin and tool that used it. Key regeneration does not disconnect separately approved OAuth apps.

Disconnect OAuth apps under Connected AI apps, or revoke their token through the OAuth endpoint. Access tokens last one hour; refresh tokens rotate and expire after 90 days. Save the replacement refresh token after each refresh.

[Refresh and revoke OAuth tokens](https://www.attriboo.com/developers/oauth#refresh-and-revoke)

### Keep report data private

The report API is intended for server-side clients. It does not provide a general cross-origin browser API. Never expose a workspace key in a browser bundle. The public tracker uses a website ID and has a different collection contract.

Sales can contain customer emails and visitor identifiers. Store and display these only to people authorized to read the workspace.

[Review privacy and data handling](https://www.attriboo.com/legal/privacy)

## Dates, money and attribution

Use consistent website, date and attribution selections across reports.

### Base URL and website selection

REST base URL: https://www.attriboo.com/api/v1. All paths in endpoint pages are relative to this base. site accepts a website ID or domain and can be omitted only when the workspace has exactly one website.

Path parameters named {site} accept an ID or domain. Payment and crawler bodies use siteId and require a website ID. Server events use site and visitor_id. Preserve the exact spelling shown on each endpoint page.

### Date ranges use UTC

period defaults to 30d. Allowed selections are realtime (last 30 minutes), today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd and all. all is still constrained by available report history.

Custom dates use from=YYYY-MM-DD and to=YYYY-MM-DD. Both are required, both are inclusive, and they override period. The returned period.to timestamp is the exclusive UTC end after the selected final date. The website's saved timezone does not change these REST report boundaries.

The dedicated /stats/realtime endpoint always uses a five-minute window; its date selection differs from period=realtime on other reports.

```text
GET /api/v1/stats/sources?site=site_example&from=2026-10-01&to=2026-10-07&compare=none
```

### Comparisons and attribution models

compare defaults to previous_period; other values are mom, qoq, yoy and none. Changes are percentages, not percentage-point differences; a comparison without data can return null.

model defaults to first_touch. Choose first_touch, last_touch, linear or time_decay. Source, campaign and landing-page revenue and conversion credit follow the model. Multi-touch credit can be fractional. Attribution assigns credit to observed activity; it does not establish causation or fill gaps in tracking.

### Money, ratios and missing values

Money is integer cents in site.currency unless a field explicitly names a transaction currency. For example, 5000 means 50.00 USD on a USD website. No automatic currency conversion is offered.

Percentages are on a 0–100 scale. ROAS is a ratio: 4 means four units of recorded revenue per unit of recorded ad spend. ROAS is null when no spend is available; null should not be converted to zero.

Revenue less advertising cost is not accounting profit after fees, taxes and every other business expense.

### Reporting capacity

Reports cover available records in the latest 365 days. Monthly collection allowances and report history are separate. Core reports and supported details use database calculations for larger sites; this is not a response-time guarantee at every traffic level.

A capacity failure is an explicit error, not zero activity. Shorten the requested range or retry, and avoid presenting incomplete results as a total. Detailed row lists have their own limits and continuation behavior.

[Pagination and completeness](https://www.attriboo.com/developers/pagination)

[Larger website report guide](https://www.attriboo.com/docs/larger-website-reports)

## Pagination and completeness

Follow continuation cursors and distinguish complete totals from a limited list of rows.

### Read every returned continuation

limit is a whole number from 1 to 500, default 50, where supported. Larger-site sales, breakdowns, standard stages and live visitors can return next_cursor. Saved-funnel definitions can also be paginated.

When next_cursor is a non-empty string, call the same endpoint with cursor set to that value. Keep the original site, report, dates, model and funnel selection. Treat the cursor as opaque and URL-encode it with your HTTP client's query builder.

Stop when next_cursor is null or absent. Some smaller-site responses only return the first limited rows and omit a cursor; do not treat absence as proof you exported every stored record.

### Read a paginated source report

This server-side Node.js example follows only cursors actually returned by the API. Its rows are the available result pages, not a guaranteed historical backup.

```javascript
const rows = [];
const query = new URLSearchParams({
  site: process.env.ATTRIBOO_SITE, from: "2026-10-01",
  to: "2026-10-07", compare: "none", limit: "100"
});
do {
  const response = await fetch(
    "https://www.attriboo.com/api/v1/stats/sources?" + query,
    { headers: { Authorization: "Bearer " + process.env.ATTRIBOO_TOKEN } }
  );
  const result = await response.json();
  if (!response.ok) throw new Error(result.error?.message || "Request failed");
  rows.push(...result.data.rows);
  if (!result.data.next_cursor) break;
  query.set("cursor", result.data.next_cursor);
} while (true);
```

### Understand cursor lifetime and live data

Breakdown and detailed-report cursors expire after 24 hours and fix relative date selections. Sales cursors have their own bounded contract; use them promptly. A stale or mismatched cursor returns an error: restart from the first page.

Live-visitor continuation pages keep the original five-minute window. Reload the first request to see current visitors. Reports remain live across imports and edits; a cursor is not an immutable backup snapshot.

AI-search, time-to-purchase and a selected saved funnel have no continuation page. For ordered funnels, list with funnel=saved and then request funnel=ID.

### Check coverage when present

Database-backed responses may include coverage.complete, historyFrom, snapshotAt, total_rows and returned_rows. Complete describes the supported report computation; returned_rows describes this page. Some report families add their own coverage fields.

Headline totals can cover more rows than a visible detail table. Do not add the same total again for each page, and do not infer a complete export from a single limited page.

[Sales and refund records](https://www.attriboo.com/developers/sales)

[Saved and standard funnels](https://www.attriboo.com/developers/funnels)

## Errors, limits and retries

Handle report failures, rate limits and route-specific ingestion responses.

### Check HTTP status before reading data

Reports, website setup, server events and changelog use a status/data success envelope. Their failures use status: error with a numeric code and a human-readable message. Payment, crawler and tracker routes use the raw shapes documented on their endpoint pages.

Never replace a failed report with zeros or public sample data.

| Status | Meaning | Action |
| --- | --- | --- |
| 400 | Invalid input or cursor | Correct the parameters; restart expired pagination |
| 401 | Missing, invalid or expired credential | Use the current key or reconnect/refresh OAuth |
| 403 | Missing scope, role or website allowance | Review permission or current plan |
| 404 | Unknown or inaccessible site/report | List websites again; check the route |
| 413 | Crawler batch too large | Send no more than 500 hits |
| 429 | Rate limit reached | Wait until X-RateLimit-Reset |
| 500 | Unexpected server failure | Retry reads with bounded backoff |
| 503 | Complete report unavailable | Retry or shorten the date range |

```json
{
  "status": "error",
  "error": {
    "code": 400,
    "message": "Choose a row limit between 1 and 500."
  }
}
```

### Report API rate limit

Endpoints using the shared API authenticator enforce 120 requests per minute per credential per server instance. Read X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). This is not a global account-wide quota guarantee.

On 429, wait until the reset time, then retry with a small random delay. For transient read failures, use a bounded exponential backoff. Stop retrying unchanged 400/401/403 requests.

Crawler and normalized payment routes have different ingestion controls and do not use these report rate-limit headers. Crawler monthly caps are documented on the crawler endpoint page.

### Retry writes carefully

Normalized payments deduplicate by website, provider and external transaction ID. Reuse the same stable ID when retrying the same payment. A separate refund needs its own stable ID.

Goal calls and crawl batches do not accept a general idempotency key. Blind retries after an uncertain result can duplicate counts. Website creation reuses an existing domain within the workspace; still require the account holder's confirmed details.

[Payment and refund semantics](https://www.attriboo.com/developers/payments)

[Crawler batching and caps](https://www.attriboo.com/developers/crawls)

## OAuth for applications

Connect an application with account-holder approval, PKCE and rotating refresh tokens.

### Discover the authorization server

These URLs use the site origin, not the /api/v1 base. MCP clients can discover the resource and authorization server automatically. The REST API accepts the resulting Bearer access token with the same approved workspace and scopes.

| Method | Path | Purpose |
| --- | --- | --- |
| GET | /.well-known/oauth-authorization-server | Authorization, registration, token and revocation metadata |
| GET | /.well-known/openid-configuration | Authorization metadata alias; do not assume an OIDC ID-token flow |
| GET | /.well-known/oauth-protected-resource | MCP resource and authorization server |
| POST | /api/oauth/register | Register a client |
| GET | /oauth/authorize | Interactive account-holder approval |
| POST | /api/oauth/token | Exchange code or rotate refresh token |
| POST | /api/oauth/revoke | Revoke a token |

```bash
curl "https://www.attriboo.com/.well-known/oauth-authorization-server"
```

### Register your application

POST /api/oauth/register accepts redirect_uris (required), client_name, client_uri, logo_uri and token_endpoint_auth_method. Public clients use none. Confidential clients can use client_secret_post or client_secret_basic; save the returned secret privately.

Redirects must be HTTPS, loopback HTTP or an allowed app scheme. Fragments and unsafe schemes are rejected. Success is HTTP 201 and includes client_id, redirect_uris, grant_types, response_types, token_endpoint_auth_method and scope. Registration does not grant access to customer data.

```bash
curl -X POST "https://www.attriboo.com/api/oauth/register" \
  -H "Content-Type: application/json" \
  --data '{"client_name":"Example reporting app","redirect_uris":["https://app.example.com/callback"],"token_endpoint_auth_method":"none"}'
```

### Request interactive approval with PKCE

Generate a cryptographically random code_verifier, compute its SHA-256 digest, and encode the digest as unpadded base64url for code_challenge. Store the verifier and an unpredictable state privately for this login attempt.

Open /oauth/authorize with response_type=code, client_id, the exact registered redirect_uri, scope=analytics:read, code_challenge, code_challenge_method=S256 and state. Request analytics:read sites:write only when website setup is needed. Validate the returned state and issuer against your saved attempt before exchanging the code.

The account holder signs in and chooses/approves the workspace. There is no anonymous account-creation API, device-code endpoint or unattended provider authorization. Authorization codes expire after ten minutes and can be used once.

```text
https://www.attriboo.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=analytics%3Aread&code_challenge=YOUR_S256_CHALLENGE&code_challenge_method=S256&state=YOUR_RANDOM_STATE
```

### Exchange the authorization code

POST form-encoded parameters to /api/oauth/token. Include grant_type=authorization_code, client_id, code, redirect_uri and code_verifier. Confidential clients must also authenticate with their registered secret method.

The raw JSON result contains access_token, token_type: Bearer, expires_in: 3600, refresh_token and scope. These are sensitive credentials. Use access_token in the REST Authorization header or the MCP client.

```bash
curl -X POST "https://www.attriboo.com/api/oauth/token" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "client_id=$ATTRIBOO_CLIENT_ID" \
  --data-urlencode "code=$ATTRIBOO_CODE" \
  --data-urlencode "redirect_uri=https://app.example.com/callback" \
  --data-urlencode "code_verifier=$ATTRIBOO_CODE_VERIFIER"
```

### Refresh and revoke

Refresh with grant_type=refresh_token, client_id and refresh_token at the same token endpoint. Authenticate a confidential client as registered. Each successful refresh returns a replacement refresh token: save it atomically. A consumed refresh token cannot be used again; keep the replacement to continue the connection.

POST token to /api/oauth/revoke to disconnect. It returns an empty HTTP 200 even for an unknown token. OAuth errors use raw error and error_description fields, not the report error envelope.

```bash
curl -X POST "https://www.attriboo.com/api/oauth/token" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "client_id=$ATTRIBOO_CLIENT_ID" \
  --data-urlencode "refresh_token=$ATTRIBOO_REFRESH_TOKEN"

curl -X POST "https://www.attriboo.com/api/oauth/revoke" \
  --data-urlencode "token=$ATTRIBOO_REFRESH_TOKEN"
```

[MCP connection guide](https://www.attriboo.com/docs/mcp)

[OAuth and key permissions](https://www.attriboo.com/developers/authentication)

## Tracker and integration routes

Understand the remaining /api/v1 routes and the supported workflow for each.

### Use the endpoint's supported access path

The endpoint reference covers the public REST operations for custom integrations, plus the public tracker collection contracts. The table below accounts for the remaining /api/v1 routes. A /api/v1 prefix alone does not make a route a workspace-key API.

Dashboard routes use the signed-in Attriboo session and current website permissions. Provider callback and webhook routes are managed by the built-in integration and use provider-specific verification. Do not substitute a workspace Bearer key or send arbitrary normalized payloads to those routes.

| Methods and paths | Supported workflow |
| --- | --- |
| GET, POST /state | Dashboard workspace state and actions; signed-in session. |
| GET /dashboard-report | Dashboard reports; signed-in session and website access. |
| GET /activity | Dashboard live activity; signed-in session and website access. |
| GET, POST, DELETE /imports | Dashboard CSV preview/import/removal; owners/admins for changes. |
| POST /privacy | Dashboard privacy controls; session and website authorization. |
| GET, POST /cookieless-settings | Dashboard daily-estimate controls; session and same-origin acknowledgement for changes. |
| GET, POST /demo/state | Public demo interactions using sample activity. |
| GET /ads/{platform}/start | Interactive ad-platform connection from Setup; signed-in session/provider callback. |
| GET /ads/{platform}/callback | Interactive ad-platform connection from Setup; signed-in session/provider callback. |
| GET /integrations/stripe/config | stripe connection discovery and interactive installation flow. |
| GET /integrations/stripe/install | stripe connection discovery and interactive installation flow. |
| GET /integrations/stripe/callback | stripe connection discovery and interactive installation flow. |
| POST /integrations/stripe/webhooks | stripe-managed application events; provider verification. |
| GET /integrations/shopify/config | shopify connection discovery and interactive installation flow. |
| GET /integrations/shopify/install | shopify connection discovery and interactive installation flow. |
| GET /integrations/shopify/callback | shopify connection discovery and interactive installation flow. |
| POST /integrations/shopify/webhooks | shopify-managed application events; provider verification. |
| POST /webhooks/stripe/{siteId} | Native stripe notifications configured through Setup; provider payload and verification, not normalized payment bodies. |
| POST /webhooks/shopify/{siteId} | Native shopify notifications configured through Setup; provider payload and verification, not normalized payment bodies. |
| POST /webhooks/lemonsqueezy/{siteId} | Native lemonsqueezy notifications configured through Setup; provider payload and verification, not normalized payment bodies. |
| POST /webhooks/polar/{siteId} | Native polar notifications configured through Setup; provider payload and verification, not normalized payment bodies. |
| POST /webhooks/paddle/{siteId} | Native paddle notifications configured through Setup; provider payload and verification, not normalized payment bodies. |
| POST /webhooks/paypal/{siteId} | Native paypal notifications configured through Setup; provider payload and verification, not normalized payment bodies. |

[Connect payment providers in Setup](https://www.attriboo.com/docs/payment-connections)

[Import costs and CRM records](https://www.attriboo.com/docs/csv-imports)

[Custom normalized payment endpoint](https://www.attriboo.com/developers/payments)

### Browser tracker access

The browser tracker posts to /collect, /cookieless and /poll; each has an endpoint page in this reference. Use the official script for ordinary installation so consent choices, visitor IDs, navigation and checkout behavior stay coordinated.

OPTIONS preflight handlers are transport support, not separate data operations. Public collection uses website IDs and never requires exposing a workspace key. The report API is a separate authenticated server interface.

[Standard collection contract](https://www.attriboo.com/developers/collect)

[Cookieless pageview contract](https://www.attriboo.com/developers/cookieless)

[Post-checkout survey contract](https://www.attriboo.com/developers/poll)

[Install the supported tracker](https://www.attriboo.com/docs/installation)

## GET /sites — List websites

Find real website IDs and currencies in the authorized workspace.

Access: Workspace key or OAuth analytics:read

### Response fields

- workspace: Workspace name.
- sites: Websites available to this connection.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/sites" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/sites", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/sites",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "workspace": "Example workspace",
    "sites": [
      {
        "id": "site_example",
        "domain": "shop.example.com",
        "currency": "USD",
        "name": "Example shop",
        "timezone": "UTC"
      }
    ]
  }
}
```

Use a returned website ID or domain in report requests. A workspace with multiple websites requires an explicit site selection.

## POST /sites — Create a website

Add a website, or reuse its existing domain, within the workspace's current plan.

Access: OAuth analytics:read + sites:write · workspace owner/admin

### JSON body

- domain (required): Public domain or HTTP(S) address. Normalized to a lowercase domain without www, credentials or a port.
- confirm (required): Confirm the website details with the account holder before sending true.
- name: Website name; defaults to domain.
- currency: Supported three-letter currency. Default: USD.
- timezone: IANA timezone. Default: UTC.

### Response fields

- site: Returned website details.
- created: Whether a website was created.

### Request example

```bash
curl -X POST "https://www.attriboo.com/api/v1/sites" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "domain": "shop.example.com",
  "name": "Example shop",
  "currency": "USD",
  "timezone": "UTC",
  "confirm": true
}'
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/sites", {
  method: "POST",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN,
  "Content-Type": "application/json"
},
  body: JSON.stringify({
  "domain": "shop.example.com",
  "name": "Example shop",
  "currency": "USD",
  "timezone": "UTC",
  "confirm": true
})
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

payload = {
    "domain": "shop.example.com",
    "name": "Example shop",
    "currency": "USD",
    "timezone": "UTC",
    "confirm": True
}

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/sites",
    method="POST",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"],
        "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode("utf-8")
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD",
      "name": "Example shop",
      "timezone": "UTC"
    },
    "created": true
  }
}
```

Requires an OAuth connection with analytics:read sites:write and current workspace owner/admin access. Workspace keys cannot use this endpoint.

Does not buy an upgrade. A website plan limit returns 403. Always use the returned site rather than inventing an ID.

## GET /sites/{site}/script — Get tracking script

Retrieve an installation snippet for the actual website and its saved privacy choices.

Access: OAuth analytics:read + sites:write · workspace owner/admin

### Query parameters

- format: Installation format. Values: html, gtm, nextjs, astro, nuxt, jsx, gatsby, docusaurus, drupal. Default: html.

### Response fields

- site: Website ID.
- domain: Website domain.
- format: Selected format.
- script: Complete snippet to install once.
- preferences: Saved mode, consent, captureEmails and visitorEstimates.
- installation_guide: Matching documentation entry point.
- next: Next installation step.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/sites/site_example/script?format=html" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/sites/site_example/script?format=html", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/sites/site_example/script?format=html",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": "site_example",
    "domain": "shop.example.com",
    "format": "html",
    "script": "<script defer data-auto-identify=\"false\" data-consent=\"required\" data-site-id=\"site_example\" src=\"https://www.attriboo.com/t.js\"></script>",
    "preferences": {
      "mode": "standard",
      "consent": "required",
      "captureEmails": false,
      "visitorEstimates": false
    },
    "installation_guide": "https://www.attriboo.com/docs/installation",
    "next": "Install once using the matching platform guide. Publish the change, respect the site's consent setup, then check received activity. The snippet contains no private API key."
  }
}
```

Path parameter site is a returned website ID or domain. Requires setup OAuth and workspace owner/admin access, even though this is a GET.

Use the exact returned script: attributes change with saved privacy choices. Publish the installation before verification.

## GET /sites/{site}/verification — Verify received activity

Check whether Attriboo has received tracking activity for this website.

Access: OAuth analytics:read + sites:write · workspace owner/admin

### Response fields

- site: Website ID.
- domain: Website domain.
- collection_paused: Whether collection is paused.
- tracking_seen: Whether script_verified_at has been set.
- verified_at: Verification timestamp, or null.
- last_event_at: Latest received event timestamp, or null.
- status: paused, activity_received or waiting. Values: paused, activity_received, waiting.
- checked_at: Time this check ran.
- meaning: Explanation of verification limits.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/sites/site_example/verification" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/sites/site_example/verification", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/sites/site_example/verification",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": "site_example",
    "domain": "shop.example.com",
    "collection_paused": false,
    "tracking_seen": false,
    "verified_at": null,
    "last_event_at": null,
    "status": "waiting",
    "checked_at": "2026-10-08T12:00:00.000Z",
    "meaning": "This checks records received by Attriboo. It does not inspect your published code, prove a deployment, or verify a payment connection."
  }
}
```

Path parameter site is a website ID or domain. Requires setup OAuth and workspace owner/admin access.

waiting and paused are not successful tracking checks. This endpoint does not inspect deployed code or verify payment connections.

## GET /stats/overview — Overview

Read headline revenue, traffic, conversion and advertising metrics with comparison changes.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- metrics: Headline metrics. Revenue, revenue_per_visitor, average_order_value, ad_spend, net_profit and ai_revenue are cents; roas is a ratio or null without spend.
- changes_pct: Percent changes for revenue, visitors, orders, conversion_rate, revenue_per_visitor, avg_session and bounce_rate. Null when no comparison is available.
- notes: Calculation and completeness notes.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/overview?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/overview?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/overview?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "metrics": {
      "visitors": 100,
      "sessions": 120,
      "pageviews": 240,
      "revenue": 20000,
      "orders": 4,
      "buyers": 4,
      "conversion_rate": 4,
      "revenue_per_visitor": 200,
      "average_order_value": 5000,
      "avg_session_seconds": 60,
      "bounce_rate": 40,
      "live_visitors": 0,
      "ad_spend": 5000,
      "net_profit": 15000,
      "roas": 4,
      "ai_revenue": 5000
    },
    "changes_pct": {
      "revenue": null,
      "visitors": null,
      "orders": null,
      "conversion_rate": null,
      "revenue_per_visitor": null,
      "avg_session": null,
      "bounce_rate": null
    },
    "notes": "Money is in integer cents. changes_pct is the percent change versus the comparison period (null when that period has no data)."
  }
}
```

net_profit is recorded revenue less ad spend; it is not accounting profit after every business expense.

Report history covers available records in the last 365 days; collection allowances and reporting capacity are separate.

## GET /stats/timeseries — Time series

Read traffic and revenue over daily, weekly, monthly or quarterly buckets.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- granularity: Requested bucket size. Values: daily, weekly, monthly, quarterly. Default: daily.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- granularity: Requested bucket granularity; long ranges may be grouped more coarsely.
- points: Report records. See the fields below.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/timeseries?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/timeseries?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/timeseries?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "granularity": "daily",
    "points": [
      {
        "label": "Oct 1",
        "visitors": 100,
        "revenue": 20000,
        "orders": 4,
        "top_source": "Google Organic",
        "comparison_visitors": null,
        "comparison_revenue": null
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

Long daily ranges can be grouped weekly, and long weekly ranges monthly. Inspect returned points rather than assuming one point per day.

Larger-site results may also include from, to, gross_sales and refunds per point and a coverage object.

## GET /stats/sources — Sources

Compare recorded marketing sources and their attributed revenue.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/sources?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/sources?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/sources?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "sources",
    "rows": [
      {
        "source": "Google Organic",
        "channel": "Organic Search",
        "visitors": 100,
        "conversions": 4,
        "revenue": 20000,
        "conversion_rate": 4,
        "revenue_per_visitor": 200
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

The selected attribution model changes conversion credit and revenue. Linear credit can split a sale across sources.

## GET /stats/channels — Channels

Compare channel groups such as Organic Search, Paid Ads and AI Referral.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/channels?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/channels?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/channels?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "channels",
    "rows": [
      {
        "channel": "Organic Search",
        "visitors": 100,
        "revenue": 20000,
        "share_of_revenue_pct": 100
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

## GET /stats/campaigns — Campaigns

Read campaign revenue, recorded costs and return on ad spend.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/campaigns?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/campaigns?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/campaigns?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "campaigns",
    "rows": [
      {
        "campaign": "autumn",
        "source": "google",
        "visitors": 100,
        "conversions": 4,
        "revenue": 20000,
        "ad_spend": 5000,
        "roas": 4,
        "cost_per_acquisition": 1250,
        "net_profit": 15000
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

A missing cost connection is not evidence that a campaign is free. No currency conversion is performed.

## GET /stats/pages — Landing pages

Read landing-page visits, AI referrals, crawler activity and revenue credit.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/pages?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/pages?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/pages?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "pages",
    "rows": [
      {
        "path": "/pricing",
        "landing_visits": 100,
        "ai_referral_visits": 10,
        "ai_bot_crawls": 20,
        "conversions": 4,
        "revenue": 20000
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

## GET /stats/hostnames — Hostnames

Group visitors by the hostname where they first arrived.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/hostnames?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/hostnames?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/hostnames?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "hostnames",
    "rows": [
      {
        "hostname": "shop.example.com",
        "visitors": 100,
        "conversion_rate": 4,
        "revenue": 20000,
        "revenue_per_visitor": 200,
        "top_source": "Google Organic"
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

## GET /stats/countries — Countries

Read countries audience counts, conversion and revenue.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/countries?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/countries?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/countries?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "countries",
    "rows": [
      {
        "name": "United States",
        "country_code": "US",
        "visitors": 100,
        "buyers": 4,
        "orders": 4,
        "revenue": 20000,
        "conversion_rate": 4,
        "revenue_per_visitor": 200,
        "average_order_value": 5000,
        "share_of_visitors_pct": 100,
        "share_of_revenue_pct": 100,
        "top_source": "Google Organic"
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

## GET /stats/cities — Cities

Read cities audience counts, conversion and revenue.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/cities?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/cities?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/cities?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "cities",
    "rows": [
      {
        "name": "New York",
        "country_code": "US",
        "visitors": 100,
        "buyers": 4,
        "orders": 4,
        "revenue": 20000,
        "conversion_rate": 4,
        "revenue_per_visitor": 200,
        "average_order_value": 5000,
        "share_of_visitors_pct": 100,
        "share_of_revenue_pct": 100,
        "top_source": "Google Organic"
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

## GET /stats/devices — Devices

Read devices audience counts, conversion and revenue.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/devices?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/devices?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/devices?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "devices",
    "rows": [
      {
        "name": "Desktop",
        "visitors": 100,
        "buyers": 4,
        "orders": 4,
        "revenue": 20000,
        "conversion_rate": 4,
        "revenue_per_visitor": 200,
        "average_order_value": 5000,
        "share_of_visitors_pct": 100,
        "share_of_revenue_pct": 100,
        "top_source": "Google Organic"
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

## GET /stats/browsers — Browsers

Read browsers audience counts, conversion and revenue.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/browsers?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/browsers?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/browsers?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "browsers",
    "rows": [
      {
        "name": "Chrome",
        "visitors": 100,
        "buyers": 4,
        "orders": 4,
        "revenue": 20000,
        "conversion_rate": 4,
        "revenue_per_visitor": 200,
        "average_order_value": 5000,
        "share_of_visitors_pct": 100,
        "share_of_revenue_pct": 100,
        "top_source": "Google Organic"
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

## GET /stats/os — Operating systems

Read operating systems audience counts, conversion and revenue.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/os?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/os?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/os?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "os",
    "rows": [
      {
        "name": "Windows",
        "visitors": 100,
        "buyers": 4,
        "orders": 4,
        "revenue": 20000,
        "conversion_rate": 4,
        "revenue_per_visitor": 200,
        "average_order_value": 5000,
        "share_of_visitors_pct": 100,
        "share_of_revenue_pct": 100,
        "top_source": "Google Organic"
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

## GET /stats/goals — Goals

Read goal completion counts and the revenue of visitors who completed each goal.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- rows: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/goals?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/goals?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/goals?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "report": "goals",
    "rows": [
      {
        "goal": "Started trial",
        "completions": 20,
        "visitors": 15,
        "revenue_from_completers": 20000,
        "completer_conversion_rate": 26.7,
        "share_of_buyers_pct": 100
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

Revenue from completers is an association, not proof the goal caused a purchase. A buyer can complete several goals.

## GET /stats/realtime — Live visitors

Read visitors active in the last five minutes.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- window_minutes: Fixed five-minute live window.
- live_visitors: Total live visitors, including later pages.
- visitors: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/realtime?site=site_example&limit=50" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/realtime?site=site_example&limit=50", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/realtime?site=site_example&limit=50",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "window_minutes": 5,
    "live_visitors": 1,
    "visitors": [
      {
        "visitor_id": "v_example",
        "country_code": "US",
        "city": "New York",
        "source": "Google Organic",
        "page": "/pricing",
        "device": "Desktop",
        "seconds_ago": 12
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

This is a fixed five-minute live window. period=realtime on other reports instead selects the last 30 minutes.

Continuation pages retain the original live window. Reload the first page for a fresh current window.

## GET /stats/sales — Sales

Read recorded payments and refunds, newest first, with observed attribution details.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- sales: Report records. See the fields below.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/sales?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/sales?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/sales?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "sales": [
      {
        "id": "tx_example",
        "provider": "stripe",
        "external_id": "order_123",
        "type": "one_time",
        "amount": 5000,
        "currency": "USD",
        "customer_email": null,
        "transacted_at": "2026-10-01T12:00:00Z",
        "attributed": true,
        "first_touch": "Google Organic",
        "last_touch": "Direct",
        "touches": 2,
        "self_reported_source": null
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

An unattributed payment is still a payment. Importing history does not recreate unobserved visits.

Keep customer_email and visitor identifiers in authorized private systems.

## GET /stats/funnels — Funnels

Read independent standard stages, list saved definitions, or inspect an ordered saved funnel.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.
- cursor: Opaque next_cursor from the previous page. Preserve the original website, report, dates, model and funnel selection. Never construct a cursor.
- funnel: Omit for standard stages; saved lists definitions; a saved funnel UUID reads its exact ordered results.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- steps_definition: Standard stage labels, when no saved funnel is selected.
- kind: saved_definitions or saved for a selected saved-funnel request.
- definition: Exact saved funnel definition when funnel=ID.
- rows: Ordered saved-funnel results when funnel=ID; label, visitors, pctOfFirst, pctOfPrevious, revCents and topSource.
- funnels: Standard cohorts, or saved definitions when funnel=saved. For funnel=ID use definition and rows instead.
- next_cursor: When provided, pass this value to continue; null means the last page. Some small-site reports omit this field.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/funnels?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/funnels?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/funnels?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "funnels": [
      {
        "name": "All traffic",
        "visitors": 100,
        "buyers": 4,
        "revenue": 20000,
        "conversion_rate": 4,
        "steps": [
          {
            "step": "Visited the site",
            "visitors": 100,
            "pct_of_entry": 100,
            "drop_off_pct": 0
          },
          {
            "step": "Viewed 2+ pages",
            "visitors": 60,
            "pct_of_entry": 60,
            "drop_off_pct": 40
          },
          {
            "step": "Completed a goal",
            "visitors": 15,
            "pct_of_entry": 15,
            "drop_off_pct": 75
          },
          {
            "step": "Purchased",
            "visitors": 4,
            "pct_of_entry": 4,
            "drop_off_pct": 73.3
          }
        ]
      }
    ]
  }
}
```

### List saved definitions

```bash
curl "https://www.attriboo.com/api/v1/stats/funnels?site=site_example&from=2026-10-01&to=2026-10-07&compare=none&funnel=saved" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "kind": "saved_definitions",
    "funnels": [],
    "next_cursor": null,
    "coverage": {
      "complete": true,
      "total_rows": 0,
      "returned_rows": 0
    }
  }
}
```

### Read an ordered saved funnel

```bash
curl "https://www.attriboo.com/api/v1/stats/funnels?site=site_example&from=2026-10-01&to=2026-10-07&compare=none&funnel=YOUR_SAVED_FUNNEL_UUID" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "kind": "saved",
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "definition": {
      "id": "11111111-1111-4111-8111-111111111111",
      "name": "Checkout",
      "steps": [
        {
          "kind": "page",
          "match": "equals",
          "value": "/pricing"
        },
        {
          "kind": "purchase"
        }
      ]
    },
    "rows": [
      {
        "label": "Visited /pricing",
        "visitors": 100,
        "pctOfFirst": 100,
        "pctOfPrevious": 100,
        "revCents": 20000,
        "topSource": "Google Organic"
      },
      {
        "label": "Purchased",
        "visitors": 4,
        "pctOfFirst": 4,
        "pctOfPrevious": 4,
        "revCents": 20000,
        "topSource": "Google Organic"
      }
    ],
    "coverage": {
      "complete": true,
      "total_rows": 2,
      "returned_rows": 2
    }
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

Standard stages independently count entry-cohort members; they do not require the stages to happen in order.

funnel=saved returns kind: saved_definitions, funnels, next_cursor and coverage. Each definition includes id, name and steps.

funnel=ID returns kind: saved, period, definition, rows and coverage. Each ordered row has label, visitors, pctOfFirst, pctOfPrevious, revCents and topSource. Saved funnels have two to eight steps; a selected funnel has no cursor.

Definitions are read-only through the public API. Create and edit them in the dashboard.

## GET /stats/ai-search — AI search

Keep crawler activity, referred human visitors and revenue credit separate for each AI engine.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- engines: Report records. See the fields below.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/ai-search?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/ai-search?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/ai-search?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "engines": [
      {
        "engine": "ChatGPT",
        "bot_crawls": 20,
        "visitors": 10,
        "attributed_buyers": 1,
        "attributed_revenue": 5000,
        "conversion_rate": 10,
        "revenue_per_visitor": 500,
        "top_page": "/pricing"
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

Examples show one engine for brevity. Actual reports include the supported engines.

Crawls do not prove citations or visits. Never call crawls divided by visitors a conversion rate.

## GET /stats/time-to-purchase — Time to purchase

Group purchases by time since a preceding recorded entry visit.

Access: Workspace key or OAuth analytics:read

### Query parameters

- site: Website ID or domain. Required when the workspace has more than one website.
- period: UTC date selection. from and to override period. Values: realtime, today, yesterday, 7d, 30d, 90d, 12m, wtd, mtd, qtd, ytd, all. Default: 30d.
- from: Inclusive start date. Supply to as well.
- to: Inclusive end date. Supply from as well.
- compare: Comparison range. Values: previous_period, mom, qoq, yoy, none. Default: previous_period.
- model: How recorded sources share revenue credit. Values: first_touch, last_touch, linear, time_decay. Default: first_touch.
- filters: URL-encoded JSON array of up to twenty visitor filters, for example [{"dim":"country","op":"is","value":"US"}]. Dimensions: source, channel, campaign, entry_page, page, hostname, country, device, browser, os, goal. Operators: is, is_not. Values are exact strings up to 500 characters. All filters must match the visitor across available history; report dates then select activity for that cohort. Unmatched payments are excluded. Crawler counts remain website-wide; spend requires every filter to be source/channel/campaign. Preserve filters when paging.
- limit: Row limit for reports that support it. Default: 50.

### Response fields

- site: Website ID, domain and currency.
- period: Resolved UTC boundaries; to is the exclusive timestamp after the selected end date.
- buckets: Report records. See the fields below.
- coverage: When provided, describes completeness, snapshot time and row coverage. Row limits apply to this page, not to headline totals.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/stats/time-to-purchase?site=site_example&from=2026-10-01&to=2026-10-07&compare=none" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/stats/time-to-purchase?site=site_example&from=2026-10-01&to=2026-10-07&compare=none", {
  method: "GET",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN
}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/stats/time-to-purchase?site=site_example&from=2026-10-01&to=2026-10-07&compare=none",
    method="GET",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"]
    }
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "site": {
      "id": "site_example",
      "domain": "shop.example.com",
      "currency": "USD"
    },
    "period": {
      "from": "2026-10-01T00:00:00.000Z",
      "to": "2026-10-08T00:00:00.000Z",
      "label": "Custom",
      "compare": "none",
      "attribution_model": "first_touch"
    },
    "buckets": [
      {
        "label": "Same day",
        "orders": 4,
        "revenue": 20000,
        "share_pct": 100,
        "avg_touches": 2
      }
    ]
  }
}
```

Money is integer cents in site.currency. Percentages use a 0–100 scale.

Examples show one bucket for brevity. Refunds and payments without a preceding recorded entry visit are excluded.

## POST /events — Record a server event

Record a named goal or identify an existing browser visitor from your server.

Access: Workspace API key · server only

### JSON body

- site: Website ID or domain; required for multiple websites.
- visitor_id (required): Read window.attriboo.getVisitorId() in the browser and pass it to your backend. Up to 64 characters.
- type (required): Operation. Values: goal, identify.
- name: Required for goal; up to 120 characters.
- email: Required for identify; normalized to lowercase.
- path: Page path; up to 500 characters. Default: /.
- metadata: Optional event properties.

### Response fields

- recorded: Request accepted by ingestion.
- site: Website ID.
- type: Submitted goal or identify type.
- name: Goal name or null.

### Request example

```bash
curl -X POST "https://www.attriboo.com/api/v1/events" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "site": "site_example",
  "visitor_id": "v_example",
  "type": "goal",
  "name": "Started trial",
  "path": "/signup",
  "metadata": {
    "plan": "pro"
  }
}'
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/events", {
  method: "POST",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN,
  "Content-Type": "application/json"
},
  body: JSON.stringify({
  "site": "site_example",
  "visitor_id": "v_example",
  "type": "goal",
  "name": "Started trial",
  "path": "/signup",
  "metadata": {
    "plan": "pro"
  }
})
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

payload = {
    "site": "site_example",
    "visitor_id": "v_example",
    "type": "goal",
    "name": "Started trial",
    "path": "/signup",
    "metadata": {
        "plan": "pro"
    }
}

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/events",
    method="POST",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"],
        "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode("utf-8")
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": {
    "recorded": true,
    "site": "site_example",
    "type": "goal",
    "name": "Started trial"
  }
}
```

### Identify the same visitor

```bash
curl -X POST "https://www.attriboo.com/api/v1/events" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "site": "site_example",
  "visitor_id": "v_example",
  "type": "identify",
  "email": "buyer@example.com"
}'
```

```json
{
  "status": "success",
  "data": {
    "recorded": true,
    "site": "site_example",
    "type": "identify",
    "name": null
  }
}
```

Use a workspace key on your backend. Current OAuth grants do not include data:write.

Goals count toward the event allowance; identify calls do not. Saved exclusions and collection controls still apply. An accepted response does not override those controls.

There is no general idempotency-key parameter. Retrying a goal request can add another completion.

## POST /webhooks/payment — Record a payment or refund

Send a normalized server-side payment with a stable provider transaction ID.

Access: Workspace API key · server only

### JSON body

- siteId (required): Website ID, not a domain.
- provider: Supported provider label; do not send an arbitrary custom string. Values: stripe, shopify, lemonsqueezy, polar, woocommerce, paddle, paypal. Default: stripe.
- externalTxId (required): Stable transaction ID. Deduplication uses website, provider and this ID.
- amountCents (required): Integer cents. Refunds are stored as negative absolute amounts.
- currency: Three-letter transaction currency. Default: USD.
- txType: Transaction type. Values: one_time, subscription_new, subscription_renewal, refund. Default: one_time.
- visitorId: Observed visitor ID, when available.
- customerEmail: Email for matching a previously identified visitor.
- customerName: Optional customer name.

### Response fields

- ok: Whether the request succeeded.
- attributedToVisitor: Resolved visitor ID or null.

### Request example

```bash
curl -X POST "https://www.attriboo.com/api/v1/webhooks/payment" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "siteId": "site_example",
  "provider": "woocommerce",
  "externalTxId": "order_123",
  "amountCents": 5000,
  "currency": "USD",
  "txType": "one_time",
  "visitorId": "v_example"
}'
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/webhooks/payment", {
  method: "POST",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN,
  "Content-Type": "application/json"
},
  body: JSON.stringify({
  "siteId": "site_example",
  "provider": "woocommerce",
  "externalTxId": "order_123",
  "amountCents": 5000,
  "currency": "USD",
  "txType": "one_time",
  "visitorId": "v_example"
})
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

payload = {
    "siteId": "site_example",
    "provider": "woocommerce",
    "externalTxId": "order_123",
    "amountCents": 5000,
    "currency": "USD",
    "txType": "one_time",
    "visitorId": "v_example"
}

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/webhooks/payment",
    method="POST",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"],
        "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode("utf-8")
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "ok": true,
  "attributedToVisitor": "v_example"
}
```

### Record a partial refund

```bash
curl -X POST "https://www.attriboo.com/api/v1/webhooks/payment" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "siteId": "site_example",
  "provider": "woocommerce",
  "externalTxId": "refund_123_1",
  "amountCents": 1000,
  "currency": "USD",
  "txType": "refund",
  "visitorId": "v_example"
}'
```

```json
{
  "ok": true,
  "attributedToVisitor": "v_example"
}
```

Response uses { ok, attributedToVisitor }, not the report status/data envelope. Errors use { error: string }.

Unmatched payments stay unattributed. No transaction timestamp parameter is supported: this endpoint is not a historical-import API.

Use stable IDs when retrying. Give a separate refund its own stable ID so it does not overwrite the original sale. Avoid sending the same payment through overlapping native and custom paths.

Built-in payment connections manage their own provider-specific webhooks. This endpoint accepts the normalized fields shown here, not native provider payloads.

## POST /crawls — Record AI crawler requests

Report server-observed AI crawler requests as a batch of daily bot/page/status counts.

Access: Workspace API key · server only

### JSON body

- siteId (required): Website ID.
- sourceIntegration: Reporting integration. Values: cloudflare, wordpress, middleware. Default: cloudflare.
- hits (required): At most 500 entries. Each has botName, urlPath (default /), statusCode (default 200) and count (default 1, capped at 1,000,000).

### Response fields

- ok: Whether the request succeeded.
- kept: Kept crawl count.
- used: Workspace usage this month; may be absent if all bots were ignored.
- limit: Workspace monthly crawl cap.
- capped: Whether the monthly cap was reached.

### Request example

```bash
curl -X POST "https://www.attriboo.com/api/v1/crawls" \
  -H "Authorization: Bearer $ATTRIBOO_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "siteId": "site_example",
  "sourceIntegration": "middleware",
  "hits": [
    {
      "botName": "OAI-SearchBot",
      "urlPath": "/pricing",
      "statusCode": 200,
      "count": 3
    }
  ]
}'
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/crawls", {
  method: "POST",
  headers: {
  "Authorization": "Bearer " + process.env.ATTRIBOO_TOKEN,
  "Content-Type": "application/json"
},
  body: JSON.stringify({
  "siteId": "site_example",
  "sourceIntegration": "middleware",
  "hits": [
    {
      "botName": "OAI-SearchBot",
      "urlPath": "/pricing",
      "statusCode": 200,
      "count": 3
    }
  ]
})
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

payload = {
    "siteId": "site_example",
    "sourceIntegration": "middleware",
    "hits": [{
        "botName": "OAI-SearchBot",
        "urlPath": "/pricing",
        "statusCode": 200,
        "count": 3
    }]
}

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/crawls",
    method="POST",
    headers={
        "Authorization": "Bearer " + os.environ["ATTRIBOO_TOKEN"],
        "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode("utf-8")
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "ok": true,
  "kept": 3,
  "used": 3,
  "limit": 100000,
  "capped": false
}
```

Uses the workspace key. OAuth tokens are not accepted. This route does not use the report rate-limit headers.

Unknown bot names are ignored. Supported names: OAI-SearchBot, ChatGPT-User, GPTBot, Claude-SearchBot, Claude-User, ClaudeBot, Perplexity-User, PerplexityBot, MistralAI-User, DuckAssistBot, meta-externalagent, Amazonbot, Bytespider and CCBot.

Legacy single-hit bodies can send siteId, botName, urlPath, statusCode and sourceIntegration without hits. Prefer batches for new integrations.

Crawls beyond the monthly cap are dropped. Free workspaces keep up to 100,000 a month; paid workspaces up to 5,000,000. This is separate from pageview/goal allowances.

Counts are additive. Retrying a successful batch can count it twice. Crawls are not visits or citations. Error responses contain error and may also contain ok: false.

## POST /collect — Collect standard browser activity

The standard tracker transport for pageviews, goals and identity calls. Prefer the official t.js installation.

Access: Public · no authentication

### JSON body

- siteId (required): Public website ID.
- visitorId (required): Observed visitor identifier, up to 64 characters.
- eventType: Activity type. Values: pageview, custom_goal, identify. Default: pageview.
- urlPath: Page path, up to 500 characters. Default: /.
- referrer: Optional recorded acquisition context.
- utmSource: Optional recorded acquisition context.
- utmMedium: Optional recorded acquisition context.
- utmCampaign: Optional recorded acquisition context.
- utmContent: Optional recorded acquisition context.
- utmTerm: Optional recorded acquisition context.
- clickIdType: Optional recorded acquisition context.
- rootDomain: Validated against the request Origin/Referer host for subdomain navigation.
- eventName: Goal name, up to 120 characters.
- email: Identity email, only with the site's authorized identity collection.
- metadata: Optional event properties.

### Response fields

- ok: Request accepted.
- channelGroup: Classified channel.
- sourceName: Classified source.

### Request example

```bash
curl -X POST "https://www.attriboo.com/api/v1/collect" \
  -H "Content-Type: application/json" \
  --data '{
  "siteId": "site_example",
  "visitorId": "v_example",
  "eventType": "pageview",
  "urlPath": "/pricing",
  "referrer": "https://www.google.com/"
}'
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/collect", {
  method: "POST",
  headers: {
  "Content-Type": "application/json"
},
  body: JSON.stringify({
  "siteId": "site_example",
  "visitorId": "v_example",
  "eventType": "pageview",
  "urlPath": "/pricing",
  "referrer": "https://www.google.com/"
})
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

payload = {
    "siteId": "site_example",
    "visitorId": "v_example",
    "eventType": "pageview",
    "urlPath": "/pricing",
    "referrer": "https://www.google.com/"
}

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/collect",
    method="POST",
    headers={
        "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode("utf-8")
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "ok": true,
  "channelGroup": "Organic Search",
  "sourceName": "Google Organic"
}
```

Public website IDs are not API credentials. Never attach a workspace key to a browser collection request.

Use t.js to coordinate consent, local exclusions, visitor identity, SPA navigation and checkout behavior. Call this transport only after applying the site's tracking and consent choices; a raw request does not implement the browser's consent UI.

The tracker sends a text/plain JSON body with fetch keepalive to avoid a preflight; application/json bodies are also parsed. Browser requests supply Origin/Referer for hostname handling. The cURL body shows the contract, not a substitute for a real browser installation.

Bot user agents return HTTP 202 with { ok: true, ignored: bot }. Other accepted events can still be excluded or dropped by collection controls. A 200 response is not proof a visit was retained.

## POST /cookieless — Collect a cookieless pageview

The c.js transport for a website already configured for aggregate measurement.

Access: Public · no authentication

### JSON body

- siteId (required): Public website ID, starting site_.
- visitorEstimates: Only daily is accepted, and only used when daily estimates are enabled in saved settings. Values: daily.

### Response fields

- ok: Whether the aggregate pageview was recorded.

### Request example

```bash
curl -X POST "https://www.attriboo.com/api/v1/cookieless" \
  -H "Origin: https://shop.example.com" \
  -H "Content-Type: application/json" \
  --data '{
  "siteId": "site_example"
}'
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/cookieless", {
  method: "POST",
  headers: {
  "Origin": "https://shop.example.com",
  "Content-Type": "application/json"
},
  body: JSON.stringify({
  "siteId": "site_example"
})
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

payload = {
    "siteId": "site_example"
}

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/cookieless",
    method="POST",
    headers={
        "Origin": "https://shop.example.com",
        "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode("utf-8")
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "ok": true
}
```

Prefer the c.js snippet returned for the site's saved aggregate mode. The JSON body is limited to 256 characters and rejects other keys.

A matching browser Origin header is required. A server-side cURL call without that header returns 403. The example must add Origin: https://shop.example.com for the example website.

No visitor ID, path, email, referrer, goal or payment fields are accepted. Daily visitor estimation requires both saved opt-in and visitorEstimates: daily. Bot user agents return 202 with { ok: true, ignored: bot }.

## POST /poll — Record a checkout survey answer

The tracker transport for a post-checkout self-reported source answer.

Access: Public · no authentication

### JSON body

- siteId (required): Website ID.
- source (required): Survey answer, stored up to 120 characters.
- visitorId: Visitor whose latest transaction receives the answer; required unless txId is supplied.
- txId: External transaction ID; used instead of visitorId when supplied.

### Response fields

- ok: Whether the request succeeded.
- matched: Whether a transaction was found.

### Request example

```bash
curl -X POST "https://www.attriboo.com/api/v1/poll" \
  -H "Content-Type: application/json" \
  --data '{
  "siteId": "site_example",
  "visitorId": "v_example",
  "source": "A colleague"
}'
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/poll", {
  method: "POST",
  headers: {
  "Content-Type": "application/json"
},
  body: JSON.stringify({
  "siteId": "site_example",
  "visitorId": "v_example",
  "source": "A colleague"
})
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

payload = {
    "siteId": "site_example",
    "visitorId": "v_example",
    "source": "A colleague"
}

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/poll",
    method="POST",
    headers={
        "Content-Type": "application/json"
    },
    data=json.dumps(payload).encode("utf-8")
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "ok": true,
  "matched": true
}
```

Prefer the built-in post-checkout survey. Supply visitorId or txId. The answer is attached to the latest matching website transaction.

matched: false means no transaction was found; it does not create a sale. Self-reported source is separate from observed attribution credit.

## GET /changelog — Read release notes

Read public release notes, newest first, without authentication.

Access: Public · no authentication

### Query parameters

- limit: Number of entries, clamped to 1–200. Default: 50.

### Response fields

- data[]: The data value is an array of release notes, not an object.

### Request example

```bash
curl "https://www.attriboo.com/api/v1/changelog?limit=10"
```

### JavaScript request

```javascript
const response = await fetch("https://www.attriboo.com/api/v1/changelog?limit=10", {
  method: "GET",
  headers: {}
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result);
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/changelog?limit=10",
    method="GET",
    headers={}
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
    print(result)
```

### Response example

```json
{
  "status": "success",
  "data": [
    {
      "date": "2026-10-01",
      "category": "New",
      "title": "Example release",
      "summary": null,
      "link": null
    }
  ]
}
```

The response is { status: success, data: [...] }. This public route allows cross-origin reads.

## GET /integrations/wordpress-plugin — Download WordPress plugin

Download the current WordPress plugin as attriboo.zip.

Access: Public · no authentication

### Request example

```bash
curl "https://www.attriboo.com/api/v1/integrations/wordpress-plugin" \
  --output attriboo.zip
```

### JavaScript request

```javascript
import { writeFile } from "node:fs/promises";

const response = await fetch("https://www.attriboo.com/api/v1/integrations/wordpress-plugin", {
  method: "GET",
  headers: {}
});
if (!response.ok) throw new Error(await response.text());
await writeFile("attriboo.zip", Buffer.from(await response.arrayBuffer()));
```

### Python request

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://www.attriboo.com/api/v1/integrations/wordpress-plugin",
    method="GET",
    headers={}
)
with urllib.request.urlopen(request) as response:
    with open("attriboo.zip", "wb") as output:
        output.write(response.read())
```

Returns application/zip with Content-Disposition: attachment; filename=attriboo.zip. This is a binary download, not JSON.

Upload in WordPress → Plugins → Add New → Upload Plugin. Enter the website ID and workspace key privately in Settings → Attriboo.
