Factory Analytics API
REST API for Factory Standard Credits consumption, tool usage, activity, and productivity metrics, plus Advanced Analytics data: intent, linked issues and pull requests, and cost attribution per session, pull request, ticket, and project (/sessions, /prs, /tickets, /projects).
The Factory Analytics API returns organization-level and self-scoped usage data for Factory. Query Factory Standard Credits consumption, tool invocations, user activity, and productivity metrics across your organization or for the authenticated user.
In Private Preview, /sessions returns one row per root session, and three cost attribution routes return one row per pull request, ticket, or project with the Factory Standard Credits attributed to it. Intent, linked issues and pull requests, and attribution are Advanced Analytics data.
Authentication
All requests require a Factory API key in the Authorization header.
Authorization: Bearer fk-your-api-keyGenerate API keys in the Factory API keys settings.
Permissions
Organization-level routes need Enterprise, and a caller with the Manager or Owner role, using their access token or their own API key. /sessions, /prs, /tickets, /projects, and /cost/by-user-trigger also accept a service-account key created by a Manager or Owner; the other routes refuse service-account keys with 403 insufficient_role. Those five routes also refuse keys that Factory issues to computers, sandboxes, and delegations, with 403 credential_not_allowed.
| Route | Enterprise | Manager or Owner role | Private Preview allowlist | Advanced Analytics consent |
|---|---|---|---|---|
/tokens, /cost/by-user-trigger, /tools, /activity, /productivity, /users | Required, else 403 enterprise_required | Required, else 403 insufficient_role | Not required | Not required |
/cost/me/query | Required, else 403 enterprise_required | Not required: any member, own data only | Not required | Not required |
/sessions | Required, else 403 enterprise_required | Required, else 403 insufficient_role | Required, else 403 preview_access_required | Optional. Without it, Advanced Analytics fields are null and Advanced Analytics filters return 403 advanced_analytics_required |
/prs, /tickets, /projects | Required, else 403 enterprise_required | Required, else 403 insufficient_role | Required, else 403 preview_access_required | Required, else 403 advanced_analytics_required |
To join the Private Preview, contact your account team. To turn on Advanced Analytics, see Enable Agent Effectiveness.
Base URL
https://api.factory.ai/api/v1/analyticsResponse format
All responses follow a consistent envelope structure:
{
"data": [ ... ],
"meta": { ... }
}| Field | Type | Description |
|---|---|---|
data | array | Array of result objects (one per day, or per group when using group_by) |
meta | object | Request metadata: org_id, start_date, end_date, and pagination fields on paginated routes |
Pagination
/sessions, /prs, /tickets, and /projects return pages of rows. Grouped /prs responses are not paginated.
Query Parameters
100.meta.next_cursor from the previous page. Send it with the same filters; a cursor sent with other filters returns 400.Meta Fields
null on the last page.null on later pages.Request pages until meta.has_more is false:
import os
import requests
url = "https://api.factory.ai/api/v1/analytics/sessions"
headers = {"Authorization": f"Bearer {os.environ['FACTORY_API_KEY']}"}
params = {"startDate": "2026-10-01", "endDate": "2026-10-31", "limit": 500}
rows = []
while True:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
body = response.json()
rows.extend(body["data"])
if not body["meta"]["has_more"]:
break
params["cursor"] = body["meta"]["next_cursor"]/users paginates differently: limit is 1-100 or all, and cursor is a user ID. See its parameters.
Endpoints
The following endpoints cover usage and productivity metrics:
| Endpoint | Description |
|---|---|
/sessions | One row per root session with its linked issues and pull requests (Private Preview) |
/tokens | Organization-wide Factory Standard Credits by model and user |
/cost/me/query | Self-scoped Factory Standard Credits and attribution metrics |
/cost/by-user-trigger | Each billed user's spend, split by who triggered it |
/tools | Tool invocations and autonomy metrics |
/activity | Daily, weekly, and monthly active users |
/productivity | File operations and git activity |
/users | Per-user metrics with pagination |
Advanced Analytics adds three cost attribution routes:
| Route | One row per |
|---|---|
/prs | Pull request |
/tickets | Linear issue or Jira issue |
/projects | Linear project or Jira epic |
Understanding group_by
Several endpoints support a group_by parameter:
-
Without
group_by: Returns one row per day with nested breakdowns (e.g.,by_tool,daily_active_users_by_client). Use this when you want all dimensions in a single response. -
With
group_by: Flattens one of those nested arrays into separate rows. Each row has agroup_keyfield identifying the dimension value. Use this when piping data into tools that expect flat rows (spreadsheets, BI tools, time-series databases).
For example, /activity without group_by returns daily_active_users_by_client as an object. With group_by=client, you get separate rows for terminal-ui, web, and non-interactive-cli - useful for plotting each client type as its own line on a chart.
Sessions
For access, see Permissions. Shares pagination, filters, and terms with the cost attribution routes; see Joining rows to connect its rows to theirs.
Returns one row per root session whose completed_at falls in the window. Sub-agent sessions are folded into their root. This is the only route with identity lookups: by session id, mission id, issue key, or pull request URL.
Query Parameters
completed_at, YYYY-MM-DD. The window may span up to 366 days.YYYY-MM-DD.ENG-123. Matches any issue linked in the subtree. Requires Advanced Analytics.true keeps rows with at least one issue or pull request; false keeps the rest. Requires Advanced Analytics.linear or jira. Keeps rows with at least one issue from this tracker. Requires Advanced Analytics.primary_intent below for the values. Requires Advanced Analytics.titles, owners, link_metadata, duration. See Optional Fields below.user returns one row per owner instead of one row per session. See Grouped Response below.100. See Pagination.meta.next_cursor from the previous page.Response
{
"data": [
{
"session_id": "sess_01HPMQ7NXKHM7Y7PR3TTZY3JZS",
"owner_user_id": "user_01HPMQ7NXKHM7Y7PR3TTZY3JZS",
"work_role": "engineering",
"started_at": "2026-10-20T14:02:11Z",
"completed_at": "2026-10-20T15:40:03Z",
"session_count": 3,
"mission_id": null,
"mission_role": null,
"automation_id": null,
"factory_standard_credits": 1820,
"primary_intent": "code_generation",
"issues": [
{
"provider": "linear",
"issue_id": "9f1c2b3a-4d5e-4f60-8a7b-1c2d3e4f5a6b",
"key": "ENG-123",
"url": "https://linear.app/acme/issue/ENG-123/add-checkout-retries",
"project_id": "7a8b9c0d-1e2f-4a3b-9c4d-5e6f7a8b9c0d",
"project_name": "Checkout v2",
"epic_id": null,
"epic_key": null,
"epic_name": null,
"status_category": "completed"
}
],
"pull_requests": [
{
"url": "https://github.com/acme/shop/pull/481",
"provider": "github",
"repo": "acme/shop",
"number": 481
}
]
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE7Y7PR3TTZY4KLM",
"start_date": "2026-10-01",
"end_date": "2026-10-31",
"has_more": true,
"next_cursor": "eyJ2IjoxLCJrIjoi...",
"total_count": 517
}
}Response Fields
work_roles filter; filter on this field client-side.orchestrator or worker.code_generation, debugging, code_review, knowledge_qa, devops, data, documentation, research, planning, automation, agent_config, noise, other, unknown. null without Advanced Analytics.null without Advanced Analytics; [] when none were found.null without Advanced Analytics; [] when none were found.issues[]
linear or jira.ENG-123.url on /tickets.null for Linear.pull_requests[]
url on /prs.github or gitlab.owner/name, lower-cased. GitLab: full namespace path.Optional Fields
include adds keys to each row. A key appears only when you request it.
| Value | Adds |
|---|---|
titles | session_title: the root session's title. null when it has none. |
owners | owner: the resolved owner (shape below). null when the owner cannot be resolved. |
link_metadata | title on each issues[] item, and title, status, and author on each pull_requests[] item. null when the item is not synced. |
duration | duration_seconds: completed_at - started_at in whole seconds. |
owner and pull_requests[].author
pull_requests[].status
unknown, open, in_progress, completed, canceled, paused, draft, merged, closed, mixed. Treat any other value as unknown.Merged.Grouped Response
With group_by=user, each row is one owner instead of one session. The other filters still select which root sessions are folded into each owner's row.
null without Advanced Analytics.null without Advanced Analytics.null without Advanced Analytics.completed_at among those roots.include=owners above. Present only with include=owners.Example
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/sessions?startDate=2026-10-01&endDate=2026-10-31&attributed=true&limit=100"Factory Standard Credits usage
Returns daily Factory Standard Credits consumption across your organization.
Query Parameters
YYYY-MM-DD format. No cap on the range.YYYY-MM-DD formatmodel or reasoning_effort to group results by that dimensionResponse Fields
YYYY-MM-DD formatnull when this value is not reported. null does not indicate zero cache usage.null when this value is not reported. null does not indicate zero cache usage.Example
# Factory Standard Credits usage for a date range
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/tokens?startDate=2026-01-14&endDate=2026-01-28"
# Grouped by model
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/tokens?startDate=2026-01-15&endDate=2026-01-15&group_by=model"Personal Factory Standard Credits usage
Returns Factory Standard Credits and attribution metrics for the authenticated user. The API derives the user ID from the bearer credential and does not accept a user selector, so callers cannot query another user.
Any member of an Enterprise organization can call it; see Permissions.
Query Parameters
data.YYYY-MM-DD format. The range may span up to 90 days.YYYY-MM-DD format.Query Keys
| Value | Description |
|---|---|
headline_daily | Daily Factory Standard Credits consumed |
cost_summary | Total sessions, messages, credits, and averages |
top_sessions | Sessions with the highest credit consumption |
by_model | Credit consumption by model |
fsc_by_model_daily | Daily credit consumption grouped by model |
fsc_by_multiplier_daily | Daily credit consumption grouped by multiplier |
by_ticket | Credit consumption attributed to tickets |
by_pr | Credit consumption attributed to pull requests |
my_activity_summary | Activity totals for the authenticated user |
my_time_spent | Estimated time spent by the authenticated user |
my_intent_breakdown | Credit consumption grouped by intent |
my_completed_tickets_daily | Daily completed-ticket counts |
my_completed_tickets | Completed-ticket details |
my_merged_prs_daily | Daily merged-pull-request counts |
my_merged_pr_detail | Merged-pull-request details |
Response
The fields inside data depend on queryKey. For headline_daily, each row contains:
{
"data": [
{
"date": "2026-08-01",
"fsc": 123456
}
],
"meta": {
"org_id": "org_01HPMQ8ABCDE7Y7PR3TTZY4KLM",
"start_date": "2026-08-01",
"end_date": "2026-08-31"
}
}Response Fields
queryKey, scoped to the authenticated user.Example
# Daily personal usage
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/cost/me/query?queryKey=headline_daily&startDate=2026-08-01&endDate=2026-08-31"Spend by triggering user
Returns each billed user's spend in the window, split by who triggered it. A service account's delegated sessions break down by the person who started each one.
Query Parameters
YYYY-MM-DD format. The range may span up to 90 days.YYYY-MM-DD format.Response
{
"data": [
{
"user_id": "user_01HPMQ7NXKHM7Y7PR3TTZY3JZS",
"user_email": "developer@company.com",
"triggered_by_user_id": "user_01HPMQ7NXKHM7Y7PR3TTZY3JZS",
"triggered_by_email": "developer@company.com",
"fsc": 5230,
"sessions": 14
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE7Y7PR3TTZY4KLM",
"start_date": "2026-10-01",
"end_date": "2026-10-31"
}
}Response Fields
sa_ prefix). null for billing rows with no user id; those rows group by user_email.null when that user has no email in the session's organization.Example
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/cost/by-user-trigger?startDate=2026-10-01&endDate=2026-10-31"Tool usage
Returns daily tool invocations, MCP usage, skills, slash commands, and autonomy metrics.
Query Parameters
YYYY-MM-DD format. The range may span up to 90 days.YYYY-MM-DD formattool_name to group results by toolResponse
{
"data": [
{
"date": "2026-01-15",
"tool_calls": 45000,
"by_tool": [
{ "tool": "Read", "invocations": 12500 },
{ "tool": "Edit", "invocations": 8200 },
{ "tool": "Execute", "invocations": 6100 }
],
"mcp_users_with_mcp": 42,
"mcp_by_server": [
{ "server": "github", "invocations": 1200 },
{ "server": "notion", "invocations": 850 }
],
"skills_invocations": 320,
"skills_by_name": [
{ "name": "browser", "count": 180 },
{ "name": "frontend-ui", "count": 95 }
],
"slash_commands_invocations": 1500,
"slash_commands_by_name": [
{ "name": "review", "count": 420 },
{ "name": "test", "count": 380 }
],
"hooks_invocations": 2800,
"hooks_by_event": [
{ "event": "PostToolUse", "matcher": "*.ts", "command": "eslint --fix", "count": 1200 }
],
"web_users": 42,
"autonomy_ratio_avg": 8.5,
"autonomy_ratio_p50": 6.2,
"autonomy_ratio_p90": 18.4,
"tool_calls_per_session_avg": 45.2,
"user_turns_per_session_avg": 5.3,
"tool_autonomy_level_ratio": {
"auto_high": 0.35,
"auto_medium": 0.42,
"auto_low": 0.18,
"manual": 0.05
}
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE...",
"start_date": "2026-01-15",
"end_date": "2026-01-15"
}
}Response Fields
YYYY-MM-DD formatGrouped Response
When group_by=tool_name, returns one row per tool per day inside data:
{
"data": [
{
"date": "2026-01-15",
"group_key": "Read",
"tool_calls": 12500
},
{
"date": "2026-01-15",
"group_key": "Edit",
"tool_calls": 8200
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE...",
"start_date": "2026-01-15",
"end_date": "2026-01-15"
}
}User activity
Returns daily, weekly, and monthly active users along with session counts.
Query Parameters
YYYY-MM-DD format. The range may span up to 90 days.YYYY-MM-DD formatclient to group by client typeResponse
{
"data": [
{
"date": "2026-01-15",
"daily_active_users": 128,
"weekly_active_users": 312,
"monthly_active_users": 485,
"daily_active_users_by_client": {
"terminal-ui": 95,
"web": 42,
"non-interactive-cli": 18
},
"sessions": 890,
"messages": 12500,
"user_messages": 4200
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE...",
"start_date": "2026-01-15",
"end_date": "2026-01-15"
}
}Response Fields
YYYY-MM-DD formatClient Types
| Client | Description |
|---|---|
terminal-ui | Interactive CLI sessions |
web | Factory App |
non-interactive-cli | Headless/automated CLI (droid exec) |
Grouped Response
When group_by=client, returns one row per client type per day inside data:
{
"data": [
{
"date": "2026-01-15",
"group_key": "terminal-ui",
"daily_active_users": 95
},
{
"date": "2026-01-15",
"group_key": "web",
"daily_active_users": 42
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE...",
"start_date": "2026-01-15",
"end_date": "2026-01-15"
}
}Productivity
Returns daily file operations and git activity.
Query Parameters
YYYY-MM-DD format. The range may span up to 90 days.YYYY-MM-DD formatResponse
{
"data": [
{
"date": "2026-01-15",
"files_created": 245,
"files_edited": 1820,
"by_extension": [
{ "extension": ".ts", "count": 890 },
{ "extension": ".tsx", "count": 420 },
{ "extension": ".py", "count": 310 }
],
"by_language": [
{ "language": "TypeScript", "count": 1310 },
{ "language": "Python", "count": 310 }
],
"git_commits": 156,
"git_prs_created": 42
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE...",
"start_date": "2026-01-15",
"end_date": "2026-01-15"
}
}Response Fields
YYYY-MM-DD formatPer-user metrics
Returns detailed metrics per user with cursor-based pagination.
Query Parameters
YYYY-MM-DD format. The range may span up to 90 days.YYYY-MM-DD formatall (default: 20)next_cursor)Response
{
"data": [
{
"user_id": "user_01HPMQ7NXKHM7Y7PR3TTZY3JZS",
"user_email": "developer@company.com",
"display_name": "Dana Developer",
"date": "2026-01-15",
"tool_calls": 1250,
"billable_tokens": 450000,
"primary_model": "claude-opus-5-5",
"primary_model_tier": "standard",
"files_created": 12,
"files_edited": 85,
"git_commits": 8,
"git_prs_created": 2,
"mcp_calls": 45,
"skill_calls": 8,
"slash_commands": 22,
"hooks": 120,
"sessions": 15,
"messages": 310,
"user_messages": 62,
"assistant_messages": 118,
"autonomy_ratio": 9.2,
"delegation_level": "auto-high",
"languages": ["TypeScript", "Python", "Go"],
"academy_highest_certification": null,
"work_role": "engineering"
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE...",
"start_date": "2026-01-15",
"end_date": "2026-01-15",
"has_more": true,
"next_cursor": "user_01HPMQ8ABCDE7Y7PR3TTZY4KLM",
"total_users": 73
}
}Response Fields
YYYY-MM-DD formatstandard or thinking)null when none was chosenDelegation Levels
| Level | Description |
|---|---|
auto-high | Maximum autonomy, minimal confirmations |
auto-medium | Balanced autonomy with some confirmations |
auto-low | Limited autonomy, frequent confirmations |
spec | Specification mode, planning before execution |
manual | Full manual control, confirm each action |
Pagination
Use cursor-based pagination to iterate through users:
# First page
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/users?startDate=2026-01-15&endDate=2026-01-15&limit=50"
# Next page
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/users?startDate=2026-01-15&endDate=2026-01-15&limit=50&cursor=user_01HPMQ8ABCDE7Y7PR3TTZY4KLM"Advanced Analytics
/sessions, /prs, /tickets, and /projects are in Private Preview. For access, see Permissions.
Advanced Analytics data is the intent of each session, the issues and pull requests it linked, and the cost attributed to each pull request, ticket, and project. It requires the Advanced Analytics enterprise control; see Enable Agent Effectiveness. The same data powers the Effectiveness dashboard. Use it to build your own ROI reporting: how much agent spend went into each pull request, ticket, project, or session, sliced by repository, project, user, date range, or state.
What you can pull
| Route | One row per | Each row carries |
|---|---|---|
/sessions | Root session, sub-agents folded in | Owner, lifetime Factory Standard Credits, primary intent, linked issues and pull requests |
/prs | Pull request | State and timestamps, line statistics, author, attributed sessions and credits |
/tickets | Linear issue or Jira issue | State, estimate, assignee, parent, project, attributed sessions and credits |
/projects | Linear project or Jira epic | State, dates, linked ticket count, attributed sessions and credits |
Shared conventions
These routes and /sessions share the response format, pagination, and the conventions below.
Filters
| Rule | Detail |
|---|---|
| Lists | Comma-separated. user_ids and session_ids accept up to 200 values; repo and project up to 50. |
| Unknown parameters | Ignored. |
Unknown state | 400. Dropping a value would widen the result. |
Unknown work_roles | Dropped. |
include=titles | Adds the provider-reported title to every row. |
Terms
| Term | Meaning |
|---|---|
| Root session | A session a user started, with its sub-agent sessions folded in. A mission's orchestrator and each worker are separate roots that share mission_id. |
| Attribution | The root sessions whose subtree linked an item, through a pull request URL or an issue key. attribution.factory_standard_credits sums the lifetime FSC of those roots. A root that linked three items counts fully under each; the column is not a share of organization spend. |
| FSC | Factory Standard Credits, the billing unit, as a raw number. |
work_role | The role a member chose at onboarding. In a work_roles filter, unknown selects members who chose none. |
date_basis | On /projects and /tickets: which timestamp the window and the page order apply to. |
Stable id | On /projects and /tickets: UUIDv5 of provider:site_id:external_id under namespace 8a6d23b3-391b-4048-b001-65e10778367c. Unchanged by renames or key changes, and identical across organizations connected to the same site. |
| Canonical PR URL | Lower-cased host and path, no query or fragment. The same string identifies a pull request on /prs and in /sessions pull_requests[]. |
Using the data
| Question | Answer |
|---|---|
| What did one pull request, ticket, or project cost? | Its attribution.factory_standard_credits. See Attribution under Terms. |
| What did the organization spend? | /tokens, which counts spend on the day it occurred. Never sum factory_standard_credits across the rows of /prs, /tickets, or /projects; the overlap between rows makes that sum overstate spend. |
| What did a set of items cost together? | Union their root_session_ids, then sum each session once from /sessions (session_ids=). Do not add ticket rows to project rows or pull request rows to ticket rows: a project's value already covers its tickets' sessions (see the project cost rule), and a session that linked both a pull request and a ticket appears under both. A row with session_ids_truncated: true lists only 500 of its roots, so a union built from it is incomplete. |
| How much work is attributed at all? | Compare attributed=true with attributed=false counts on /sessions, /prs, /tickets, and /projects. An unattributed item had no root session link it through a pull request URL or an issue key; its factory_standard_credits is 0. |
| Which numbers tie? | A pull request, ticket, or project row equals the sum of /sessions factory_standard_credits over its root_session_ids, unless truncated. Reconcile tickets to projects only through root_session_ids. /sessions summed over a window does not equal /tokens for the same dates: /sessions places a root's lifetime spend on its completed_at, while /tokens counts spend on the day it occurred. They converge over long windows; they do not match. |
| Which dates does a window select? | /prs: state_at, when the pull request entered its current state. /tickets and /projects: a linked session's date (date_basis=active, the default), completed_at, or created_at. /sessions: the root's completed_at. The same startDate/endDate therefore selects different events on each route; do not compare counts across routes as if they described one population. |
Example
Pull requests merged or closed in October with at least one attributed session:
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/prs?startDate=2026-10-01&endDate=2026-10-31&attributed=true&limit=500"To page through results, see Pagination. To connect rows across routes, see Joining rows.
Pull requests
Requires Advanced Analytics
For access, see Permissions.
Returns one row per pull request the organization has synced. Each row is windowed on the moment it entered its current state: merged_at, closed_at, or opened_at for open and draft pull requests. state selects which states to list; the default, merged,closed, lists the pull requests that finished in the window.
Query Parameters
state_at, YYYY-MM-DD. The window may span up to 366 days.YYYY-MM-DD.open, draft, merged, closed. Default merged,closed. An unknown value returns 400.github or gitlab.owner/name (GitLab: namespace path). Case-insensitive. Up to 50.unknown included.true keeps pull requests with at least one linked root session; false keeps the rest.true includes bot-authored pull requests. Default false.titles adds the provider-reported title to each row.day or day,author returns daily counts instead of rows. See Grouped Response below. Returns 400 with include=titles.Response
{
"data": [
{
"url": "https://github.com/acme/shop/pull/481",
"provider": "github",
"host": "github.com",
"repo": "acme/shop",
"state": "merged",
"state_at": "2026-10-21T16:05:44Z",
"opened_at": "2026-10-19T09:12:00Z",
"merged_at": "2026-10-21T16:05:44Z",
"closed_at": "2026-10-21T16:05:44Z",
"additions": 212,
"deletions": 40,
"changed_files": 6,
"author": {
"login": "adev",
"user_id": "user_01HPMQ7NXKHM7Y7PR3TTZY3JZS",
"is_bot": false
},
"attribution": {
"session_count": 2,
"contributor_user_ids": ["user_01HPMQ7NXKHM7Y7PR3TTZY3JZS"],
"factory_standard_credits": 2410,
"first_session_at": "2026-10-19T10:00:00Z",
"last_session_at": "2026-10-21T15:30:00Z",
"root_session_ids": [
"sess_01HPMQ8ABCDE7Y7PR3TTZY4KLM",
"sess_01HPMQ7NXKHM7Y7PR3TTZY3JZS"
],
"session_ids_truncated": false
},
"source_updated_at": "2026-10-21T16:06:10Z"
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE7Y7PR3TTZY4KLM",
"start_date": "2026-10-01",
"end_date": "2026-10-31",
"state": ["merged", "closed"],
"has_more": false,
"next_cursor": null,
"total_count": 1
}
}Response Fields
github or gitlab.null when the URL shape is unknown.owner/name, lower-cased. GitLab: full namespace path.open, draft, merged, or closed. draft is an open draft; closed is closed without merging.state. The window and page order apply to it.include=titles.null when the provider sync carried no line statistics.author
null while the provider identity is unlinked.attribution
0 when unattributed.true when root_session_ids hit the cap; session_count then floors at 500.Meta Fields
state set the window was applied to.Grouped Response
With group_by=day, each row is one UTC day of state_at; days with no matching pull request are absent. Without state, only merged pull requests are counted. Grouped responses are not paginated, and meta.group_by echoes the grouping.
YYYY-MM-DD.null when none of the day's pull requests carry line statistics.With group_by=day,author, each row is one author's day and adds author (same shape as above) and work_role.
Example
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/prs?startDate=2026-10-01&endDate=2026-10-31&attributed=true&repo=acme/shop"- A pull request opened in the window but still open is listed only with
state=openorstate=draft. There is no query for "opened in the window, whatever happened since". - To look up one pull request or the pull requests of one session, use
/sessionswithpull_request_urlorsession_ids. - Bitbucket pull requests are not served.
Tickets
Requires Advanced Analytics
For access, see Permissions.
Returns one row per work item the organization has synced: a Linear issue or a Jira issue below the epic level. date_basis selects which timestamp the window applies to. project and parent narrow the list to one container's or one work item's children; there is no lookup by key, URL, or session id.
Query Parameters
YYYY-MM-DD. The window may span up to 366 days.YYYY-MM-DD.active (default) keeps work items with a linked session in the window, ordered by created_at. completed windows on completed_at and keeps completed items only. created windows on created_at.open, in_progress, completed, canceled. Returns 400 with date_basis=completed.linear or jira.none alone for work items in no container.unknown included.true keeps work items with at least one linked root session; false keeps the rest.titles adds the provider-reported title to each row.Response
{
"data": [
{
"id": "9b7a2c10-1d4e-5f6a-8b9c-0d1e2f3a4b5c",
"url": "https://linear.app/acme/issue/ENG-123/add-checkout-retries",
"key": "ENG-123",
"provider": "linear",
"external_id": "9f1c2b3a-4d5e-4f60-8a7b-1c2d3e4f5a6b",
"kind": "issue",
"parent": null,
"project": { "url": "https://linear.app/acme/project/checkout-v2-ab12" },
"state": "completed",
"story_points": 3,
"assignee": { "user_id": "user_01HPMQ7NXKHM7Y7PR3TTZY3JZS" },
"created_at": "2026-10-12T09:00:00Z",
"completed_at": "2026-10-21T16:10:00Z",
"attribution": {
"session_count": 2,
"contributor_user_ids": ["user_01HPMQ7NXKHM7Y7PR3TTZY3JZS"],
"factory_standard_credits": 2410,
"root_session_ids": [
"sess_01HPMQ8ABCDE7Y7PR3TTZY4KLM",
"sess_01HPMQ7NXKHM7Y7PR3TTZY3JZS"
],
"session_ids_truncated": false
}
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE7Y7PR3TTZY4KLM",
"start_date": "2026-10-01",
"end_date": "2026-10-31",
"date_basis": "active",
"has_more": true,
"next_cursor": "eyJ2IjoxLCJrIjoi...",
"total_count": 19
}
}Response Fields
issues[].url on /sessions.ENG-123. The value issue_key takes on /sessions.linear or jira.sub_issue when the parent is a synced work item, else issue.{ url } of the synced parent at any level. A Jira story's parent is its epic.{ url } of the container the item rolls up to. Matches a /projects row.open, in_progress, completed, or canceled.include=titles.{ user_id } of the assignee as a Factory member. null when unassigned or the provider identity is unlinked.attribution
0 when unattributed.true when root_session_ids hit the cap; session_count then floors at 500.Meta Fields
Example
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/tickets?startDate=2026-10-01&endDate=2026-10-31&date_basis=completed&attributed=true"projectandparentmust be Linear or Jira URLs; anything else returns400.project=nonecannot be combined with URLs.- Each ticket's attribution is direct: the roots that linked this work item, not its children.
Projects
Requires Advanced Analytics
For access, see Permissions.
Returns one row per container the organization has synced: a Linear project or a Jira epic. date_basis selects which timestamp the window applies to. There is no lookup by URL or session id; join from /tickets project.url or filter /sessions instead.
Query Parameters
YYYY-MM-DD. The window may span up to 366 days.YYYY-MM-DD.active (default) keeps containers with a linked session in the window, ordered by created_at. completed windows on completed_at and keeps completed containers only. created windows on created_at.open, in_progress, paused, completed, canceled. Returns 400 with date_basis=completed.linear or jira.true keeps containers with at least one linked root session; false keeps the rest.titles adds the provider-reported title to each row.Response
{
"data": [
{
"id": "4c1e0f7a-5b2d-5b1a-9c3e-2f7d8a9b0c11",
"url": "https://linear.app/acme/project/checkout-v2-ab12",
"provider": "linear",
"external_id": "7a8b9c0d-1e2f-4a3b-9c4d-5e6f7a8b9c0d",
"kind": "project",
"state": "completed",
"created_at": "2026-07-02T08:00:00Z",
"completed_at": "2026-10-15T17:20:00Z",
"attribution": {
"session_count": 2,
"contributor_user_ids": [
"user_01HPMQ7NXKHM7Y7PR3TTZY3JZS",
"user_01HPMQ8ABCDE7Y7PR3TTZY4KLM"
],
"factory_standard_credits": 9650,
"root_session_ids": [
"sess_01HPMQ8ABCDE7Y7PR3TTZY4KLM",
"sess_01HPMQ7NXKHM7Y7PR3TTZY3JZS"
],
"session_ids_truncated": false,
"linked_ticket_count": 5
}
}
],
"meta": {
"org_id": "org_01HPMQ6ABCDE7Y7PR3TTZY4KLM",
"start_date": "2026-08-01",
"end_date": "2026-10-31",
"date_basis": "completed",
"has_more": false,
"next_cursor": null,
"total_count": 1
}
}Response Fields
linear or jira.project for every row today. initiative and theme are reserved for higher levels.open, in_progress, paused, completed, or canceled.null when not synced. Present only with include=titles.attribution
0 when unattributed.true when root_session_ids hit the cap; session_count then floors at 500.Meta Fields
Example
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/projects?startDate=2026-08-01&endDate=2026-10-31&date_basis=completed"- Project cost rule: a root session that linked several tickets of one project counts once, and a root that linked the container directly counts even when it linked none of its tickets. A project's
factory_standard_creditscan therefore be above or below the sum of its tickets' values; the union ofroot_session_idsis the only safe reconciliation. linked_ticket_countcounts attributed work items, not every work item in the container.
Joining rows
| From | To | Key |
|---|---|---|
/prs url | /sessions pull_requests[].url | Canonical PR URL |
/tickets url, key | /sessions issues[].url, issue_key= | Issue URL, issue key |
/tickets project.url | /projects url | Container URL |
attribution.root_session_ids[] | /sessions session_id | Root session id |
attribution.contributor_user_ids[], author.user_id, assignee.user_id | /sessions owner_user_id, /users user_id | Factory user id (null while the provider identity is unlinked) |
Important constraints
Date requirements
Format- All dates must be
YYYY-MM-DD. Timezone- UTC only (no timezone parameter).
Data availabilityendDatemay be today (UTC), which returns partial data for the current UTC day. A future date returns a400error.Historical data- Available from January 14, 2026.
Range- At most 90 days per request.
/tokenshas no cap;/sessionsand the cost attribution routes allow 366 days.
Rate limits
100 requests per minute per organization. Exceeding it returns 429. Contact us if you need higher limits for dashboard or automation use cases.
Errors
Errors carry a stable code next to title, status, detail, and requestId. Branch on code; detail is human-readable text and can change.
| Status | code | Description |
|---|---|---|
400 | invalid_parameter | Invalid date format, future end date, range over the cap, limit out of range, unknown state, or a cursor issued under other filters |
401 | Missing or invalid API key | |
403 | enterprise_required | Organization is not on Enterprise |
403 | insufficient_role | Caller does not have the Manager or Owner role, or uses a service-account key on a route that does not accept one |
403 | credential_not_allowed | Key that Factory issues to computers, sandboxes, or delegations, on /sessions, /prs, /tickets, /projects, or /cost/by-user-trigger |
403 | preview_access_required | Organization is not in the Private Preview |
403 | advanced_analytics_required | Advanced Analytics is not enabled, including an Advanced Analytics filter on /sessions |
429 | rate_limited | Rate limit exceeded. Retry after the Retry-After header |
500 | Internal error | |
503 | data_not_ready | /sessions, /prs, /tickets, or /projects data is not built yet or is rebuilding. Retry after the Retry-After header |
See Permissions for which 403 codes each route can return.
Error response format
{
"title": "Bad Request",
"code": "invalid_parameter",
"detail": "endDate cannot be in the future. Please use today (UTC) or earlier as the end date.",
"status": 400,
"requestId": "req_01HPMQ9WXYZ..."
}Data pipeline
Analytics data flows through the following pipeline:
CLI/Daemon → OTEL Events → BigQuery (raw) → dbt models → API- Source: OpenTelemetry spans from the CLI and daemon
- Processing: Daily batch aggregation via dbt
- Availability: Today's date returns partial data for the current UTC day.
/sessions,/prs,/tickets, and/projectscan return503while their data rebuilds
Data quality notes
A few known data quality considerations:
- MCP server names: Some duplicates exist due to case sensitivity (e.g.,
axiomvsAxiom) - Tool names: Approximately 0.006% of entries contain parsing artifacts
- User counts: A user active on multiple clients counts once in DAU but appears in each client breakdown
Use cases
Cost monitoring dashboard
Track usage trends and identify cost drivers:
# Daily usage for the month
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/tokens?startDate=2026-01-14&endDate=2026-01-28"Adoption tracking
Monitor DAU/WAU/MAU and identify adoption patterns:
# Activity metrics with client breakdown
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/activity?startDate=2026-01-14&endDate=2026-01-28&group_by=client"Team productivity reports
Measure output and efficiency:
# Productivity metrics
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/productivity?startDate=2026-01-14&endDate=2026-01-28"Individual performance
Pull per-user metrics for team leads:
# Paginate through all users
curl -H "Authorization: Bearer $FACTORY_API_KEY" \
"https://api.factory.ai/api/v1/analytics/users?startDate=2026-01-15&endDate=2026-01-15&limit=100"Related resources
See what Advanced Analytics measures and where it shows up in the Factory App.
Connect integrations and turn on Advanced Analytics.
Create service accounts and keys for automated reporting.
Authenticate and make your first Factory API request.