Phona Analytics API
Every Phona chat widget is also a passive analytics engine. Once the widget script is on a page, it silently records page views, traffic sources, visitor geography, device data, time-on-page, and every widget interaction. This API gives you programmatic access to all of it.
What the Phona widget tracks
The Phona embed script is a lightweight (~4 KB) snippet that loads asynchronously and never blocks page rendering. As well as powering the chat widget, it silently collects the following signals on every page where it is installed:
Page-level data
- ✓ Full URL of the page viewed
- ✓ Page title
- ✓ Time on page in seconds (sent via exit beacon when visitor leaves)
- ✓ Timestamp (UTC)
Visitor data
- ✓ IP address — hashed (SHA-256, first 16 chars) for GDPR compliance
- ✓ Country code (ISO 3166-1, from CDN headers)
- ✓ Device type (mobile / tablet / desktop)
- ✓ Browser name (Chrome, Safari, Firefox, Edge, etc.)
- ✓ Operating system (Windows, macOS, iOS, Android, Linux)
- ✓ Screen resolution (width × height)
- ✓ Browser language (e.g.
en-GB)
Traffic source
- ✓ Referrer URL (full URL of the referring page)
- ✓ Source classification — Google, Bing, Yahoo, DuckDuckGo, Facebook, Twitter/X, LinkedIn, Instagram, direct, referral
- ✓ Medium — organic, social, referral
- ✓ UTM parameters —
utm_source,utm_medium,utm_campaign,utm_content,utm_term(captured from page URL if present)
Widget interactions
- ✓ Widget opened (
widget_open) - ✓ Widget closed with duration in seconds (
widget_close) - ✓ Conversation started / ended (
conversation_start,conversation_end) - ✓ Inquiry submitted (
inquiry_submit) - ✓ Which page the widget was on when each event fired
The tracking script
If the Phona chat widget is already installed on a site, analytics tracking is already active — no additional code is needed. The same embed snippet that powers the widget also fires analytics events.
The standard embed snippet looks like this:
<!-- Phona Chat Widget -->
<script src="https://app.phona.app/widget.js"
data-assistant-id="YOUR_ASSISTANT_ID"
async></script>
Your unique data-assistant-id is shown in your Phona dashboard under the Install tab for each assistant. Analytics tracking is automatic — no extra configuration needed.
Authentication
All API requests must include your API key as a Bearer token in the Authorization header.
Authorization: Bearer phona_live_xxxxxxxxxxxxxxxxxxxx
Getting your API key
- Log into your Phona dashboard at app.phona.app
- Go to Settings → API Access
- Click Generate API Key
- Copy the key — it is only shown once
Key management endpoint
Use your Supabase session token (not your API key) to manage API keys programmatically:
/.netlify/functions/generate-api-key— list active keys/.netlify/functions/generate-api-key— create a new key/.netlify/functions/generate-api-key?id=KEY_ID— revoke a keyThe full key is returned only on creation. Only the SHA-256 hash is stored. List/delete return the key prefix (first 18 chars + ...) for identification.
Base URL & versioning
https://app.phona.app/api/v1
All responses are JSON. Dates are ISO 8601 strings in UTC. Pagination uses limit and offset query parameters.
The API version is included in the URL path. Breaking changes will increment the version. The current version is v1.
API Reference
All endpoints require the Authorization header unless stated otherwise.
Assistants
/assistants
Returns all assistants in your account. Use the id from each assistant to filter analytics by assistant.
{
"data": [
{
"id": "ast_01hx...",
"name": "Acme Corp Website",
"type": "website_widget",
"website_url": "https://acme.com",
"created_at": "2026-01-15T10:30:00Z"
}
],
"meta": { "total": 3, "limit": 20, "offset": 0 }
}
Page views
/analytics/pageviews
Aggregate page view counts over a time range, grouped by day or hour.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| assistant_id | string | required | ID of the Phona assistant |
| start_date | date | required | ISO 8601 date, e.g. 2026-01-01 |
| end_date | date | required | ISO 8601 date, inclusive |
| granularity | string | optional | day (default) or hour |
| page_url | string | optional | Filter to a specific page path, e.g. /blog/seo-tips |
{
"data": [
{ "date": "2026-04-01", "pageviews": 142, "unique_visitors": 98 },
{ "date": "2026-04-02", "pageviews": 189, "unique_visitors": 121 }
],
"summary": {
"total_pageviews": 331,
"total_unique_visitors": 219,
"avg_pageviews_per_day": 165.5
}
}
Sessions
/analytics/sessions
Returns individual visitor sessions with full metadata: entry page, exit page, pages viewed, time on site, traffic source, device, location, and widget interaction summary.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| assistant_id | string | required | ID of the assistant |
| start_date | date | required | Start of date range |
| end_date | date | required | End of date range |
| country | string | optional | ISO 3166-1 alpha-2 country code, e.g. GB |
| source | string | optional | Filter by source: google, bing, direct, social, email, referral |
| device_type | string | optional | mobile, tablet, or desktop |
| widget_engaged | boolean | optional | If true, only sessions where the visitor opened the widget |
| limit | integer | optional | Max records (default 50, max 500) |
| offset | integer | optional | Pagination offset |
{
"visitor_id": "v_a3f1b2...",
"session_date": "2026-04-15",
"started_at": "2026-04-15T14:23:11Z",
"ended_at": "2026-04-15T14:31:47Z",
"duration_seconds": 516,
"pages_viewed": 4,
"entry_page": "/blog/seo-guide",
"exit_page": "/pricing",
"traffic_source": {
"source": "google",
"medium": "organic",
"referrer_url": "https://www.google.com/",
"utm_source": null,
"utm_medium": null,
"utm_campaign": null,
"utm_content": null,
"utm_term": null
},
"visitor": {
"ip_hash": "a3f1b2c4d5e6f7a8",
"country": "GB",
"device_type": "desktop",
"browser": "Chrome",
"os": "macOS",
"screen_width": 1440,
"screen_height": 900,
"language": "en-GB"
},
"widget": {
"engaged": true,
"widget_session_seconds": 148
}
}
Traffic sources
/analytics/traffic-sources
Breakdown of sessions by traffic source over the requested period. Useful for understanding how much traffic comes from organic search vs paid vs social vs direct.
{
"data": [
{ "source": "google", "medium": "organic", "sessions": 412, "pct": 54.2 },
{ "source": "direct", "medium": null, "sessions": 189, "pct": 24.9 },
{ "source": "bing", "medium": "organic", "sessions": 71, "pct": 9.3 },
{ "source": "twitter", "medium": "social", "sessions": 44, "pct": 5.8 },
{ "source": "email", "medium": "email", "sessions": 44, "pct": 5.8 }
],
"total_sessions": 760
}
Per-page analytics
/analytics/pages
Returns a ranked list of pages with view counts, average time on page, and widget engagement rate. Invaluable for SEO audits — you can see exactly which pages are getting traffic, how long people stay, and whether they interact with the widget.
{
"data": [
{
"page_url": "/blog/seo-guide",
"page_title": "Ultimate SEO Guide 2026",
"pageviews": 892,
"unique_visitors": 741,
"avg_time_on_page_seconds": 187,
"widget_open_rate_pct": 8.3,
"top_sources": ["google", "direct"]
}
],
"meta": { "total": 47, "limit": 50, "offset": 0 }
}
Widget events
/analytics/widget-events
Raw event stream of widget interactions. Each event has a type, timestamp, and session context. Use this to build custom funnels or trigger downstream actions.
Event types
widget_open
widget_close
conversation_start
conversation_end
inquiry_submit
widget_close events include a duration_seconds field (clamped 1–14400 s) indicating how long the widget was open.
{
"data": [
{
"id": "uuid...",
"event_type": "widget_open",
"visitor_id": "v_a3f1b2...",
"session_id": "s_01hx...",
"page_url": "/pricing",
"duration_seconds": null,
"created_at": "2026-04-15T14:28:00Z"
},
{
"id": "uuid...",
"event_type": "widget_close",
"visitor_id": "v_a3f1b2...",
"session_id": "s_01hx...",
"page_url": "/pricing",
"duration_seconds": 148,
"created_at": "2026-04-15T14:30:28Z"
}
]
}
Geography
/analytics/geography
Session counts grouped by country (ISO 3166-1 alpha-2 code, from CDN headers).
{
"data": [
{ "country": "GB", "sessions": 312, "pct": 41.1 },
{ "country": "US", "sessions": 201, "pct": 26.4 },
{ "country": "AU", "sessions": 88, "pct": 11.6 }
],
"total_sessions": 760
}
Devices
/analytics/devices
Session breakdown by device type, browser, and operating system.
{
"device_types": [
{ "type": "desktop", "sessions": 412, "pct": 54.2 },
{ "type": "mobile", "sessions": 289, "pct": 38.0 },
{ "type": "tablet", "sessions": 59, "pct": 7.8 }
],
"browsers": [
{ "browser": "Chrome", "sessions": 390, "pct": 51.3 },
{ "browser": "Safari", "sessions": 241, "pct": 31.7 }
],
"operating_systems": [
{ "os": "Windows", "sessions": 298, "pct": 39.2 },
{ "os": "iOS", "sessions": 201, "pct": 26.4 },
{ "os": "macOS", "sessions": 171, "pct": 22.5 }
]
}
Data models
TrafficSource
| Field | Type | Values |
|---|---|---|
| source | string | null | google, bing, yahoo, duckduckgo, twitter, facebook, linkedin, instagram, email, direct, referral |
| medium | string | null | organic, social, email, referral |
| referrer_url | string | null | Full URL of the referring page |
| utm_source | string | null | e.g. newsletter, google |
| utm_medium | string | null | e.g. email, cpc |
| utm_campaign | string | null | e.g. april-2026 |
| utm_content | string | null | Ad variant / creative identifier |
| utm_term | string | null | Paid search keyword |
VisitorData
| Field | Type | Notes |
|---|---|---|
| ip_hash | string | null | First 16 chars of SHA-256 hash of raw IP |
| country | string | null | ISO 3166-1 alpha-2 country code from CDN headers |
| device_type | string | null | mobile, tablet, desktop |
| browser | string | null | Chrome, Safari, Firefox, Edge, etc. |
| os | string | null | Windows, macOS, iOS, Android, Linux |
| screen_width | integer | null | Screen width in logical pixels |
| screen_height | integer | null | Screen height in logical pixels |
| language | string | null | Browser language, e.g. en-GB |
WidgetEvent
| Field | Type | Notes |
|---|---|---|
| id | string | UUID |
| event_type | string | widget_open, widget_close, conversation_start, conversation_end, inquiry_submit |
| visitor_id | string | Stable per-browser identifier |
| session_id | string | null | Per-page-load session token |
| page_url | string | null | Page where the event fired |
| duration_seconds | integer | null | Seconds widget was open — only on widget_close, clamped 1–14400 |
| created_at | string | ISO 8601 UTC timestamp |
Rate limits
| Plan | Requests / minute | Requests / day | Max date range |
|---|---|---|---|
| All plans | 60 | 10,000 | 365 days |
Rate limit headers are returned on every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
Errors
All errors follow the same shape:
{
"error": {
"code": "INVALID_DATE_RANGE",
"message": "end_date must be after start_date",
"status": 400
}
}
| HTTP status | Error code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 403 | FORBIDDEN | API access requires Agency plan |
| 400 | INVALID_DATE_RANGE | end_date is before start_date, or range exceeds limit |
| 400 | MISSING_PARAMETER | A required query parameter was omitted |
| 404 | ASSISTANT_NOT_FOUND | The assistant_id does not belong to this account |
| 429 | RATE_LIMITED | Too many requests — back off and retry after X-RateLimit-Reset |
| 500 | INTERNAL_ERROR | Something went wrong on our end — contact support |
Code examples
JavaScript (fetch)
const PHONA_API_KEY = 'phona_live_xxxxxxxxxxxxxxxxxxxx';
const ASSISTANT_ID = 'ast_01hx...';
async function getPageAnalytics(startDate, endDate) {
const params = new URLSearchParams({
assistant_id: ASSISTANT_ID,
start_date: startDate,
end_date: endDate,
limit: '100'
});
const res = await fetch(
`https://app.phona.app/api/v1/analytics/pages?${params}`,
{
headers: {
'Authorization': `Bearer ${PHONA_API_KEY}`,
'Content-Type': 'application/json'
}
}
);
if (!res.ok) {
const err = await res.json();
throw new Error(err.error.message);
}
return res.json();
}
// Usage
const data = await getPageAnalytics('2026-04-01', '2026-04-21');
console.log(data.data); // array of page analytics objects
Python (requests)
import requests
PHONA_API_KEY = "phona_live_xxxxxxxxxxxxxxxxxxxx"
ASSISTANT_ID = "ast_01hx..."
BASE_URL = "https://app.phona.app/api/v1"
headers = {"Authorization": f"Bearer {PHONA_API_KEY}"}
# Get traffic source breakdown
resp = requests.get(
f"{BASE_URL}/analytics/traffic-sources",
headers=headers,
params={
"assistant_id": ASSISTANT_ID,
"start_date": "2026-04-01",
"end_date": "2026-04-21",
}
)
resp.raise_for_status()
sources = resp.json()["data"]
for src in sources:
print(f"{src['source']:12} {src['sessions']:5} sessions ({src['pct']:.1f}%)")
cURL
curl -X GET \
"https://app.phona.app/api/v1/analytics/pages?assistant_id=ast_01hx...&start_date=2026-04-01&end_date=2026-04-21" \
-H "Authorization: Bearer phona_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json"
Ready to integrate?
API access is available on all Phona plans. Create your account, install the widget, and generate your key in minutes.