> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useorbitcrm.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Reports API

> The numbers on the OrbitForms Reports page from code or an AI agent: pipeline by stage, win rate, stage conversion, time in stage, deals created and won, activity by rep, lead response time, goals and the forecast, with the records behind every number.

The **Reports** page in OrbitForms is one fixed sales dashboard. Every report on it is available here with the same filters and the same definitions, and so is the list of deals, activities or contacts behind any number. Goal attainment and the forecast are read-only here too. The [MCP server](https://docs.useorbitcrm.ai/mcp/tools.md) exposes the same numbers through `get_sales_report`, `get_goals` and `get_forecast`, and questions through `ask_report_question`.

> [!NOTE]
>
> **Numbers follow the credential owner's record access.** A rep limited to their own records gets their own numbers, exactly as on the Reports page. Records in Recently deleted never count.

## Endpoints

| Method | Endpoint | Description |
| - | - | - |
| `GET` | `/api/v1/reports/sales/:report` | One report: `pipeline`, `win_rate`, `conversion`, `stage_time`, `deal_flow`, `activity`, `response`, `renewals` or `recurring`. Answers `{ data, previous? }`; `data.period` is the range it covers |
| `GET` | `/api/v1/reports/sales/:report/rows` | The records behind one number, newest first (`page`, `per_page` up to 100). Answers `{ data, meta: { kind, total, page, per_page, total_pages, currency, period } }`; `kind` is `deals`, `activities` or `contacts` |
| `GET` | `/api/v1/goals` | [Goals and attainment](#goals-and-attainment) for a period: each person's and team's actual, goal and attainment, ranked |
| `GET` | `/api/v1/forecast` | [The forecast](#forecast) for a month or fiscal quarter, per person and team, with submissions and overrides |
| `POST` | `/api/v1/reports/questions` | [Ask a question](#ask-a-question) in plain English and get the answer as a table. Uses 1 AI credit per answer |

## Scopes

Each report needs the read scope of the data it summarizes: `deals:read` for `pipeline`, `win_rate`, `conversion`, `stage_time`, `deal_flow`, `renewals` and `recurring`; `activities:read` for `activity`; `contacts:read` for `response`.

## Filters

| Parameter | Description |
| - | - |
| `period` | `last_7_days`, `last_30_days`, `last_90_days`, `this_week`, `last_week`, `this_month`, `last_month`, `this_quarter` (default), `last_quarter`, `this_year`, `last_year` or `custom`. Quarters and years follow the fiscal year (`fiscal_year_start_month` in the deal settings) |
| `from`, `to` | A custom range: inclusive dates, `YYYY-MM-DD` (implies `period=custom`) |
| `time_zone` | IANA time zone for days and periods, e.g. `America/New_York` (default `UTC`) |
| `interval` | `day`, `week` (from Monday), `month` or `quarter`; chosen from the range when left out |
| `pipeline_id` | One pipeline (the deal reports) |
| `owner_id` | A deal or contact owner, or the rep who did the activity: user IDs or `me` (repeat, or comma-separate) |
| `group_id` | A team (member group): its members' records |
| `compare` | `true` adds `previous`: the same report for the period before. A running period ("this month") is compared with the same stretch of the one before |

```bash theme={null}
curl "https://app.useorbitcrm.ai/api/v1/reports/sales/win_rate?period=last_quarter&time_zone=America/New_York&compare=true" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

```json theme={null}
{
  "data": {
    "report": "win_rate",
    "totals": { "won": 12, "lost": 18, "won_amount": 184000, "lost_amount": 251000, "win_rate": 0.4, "average_won_amount": 15333.33, "average_days_to_close": 34.5, "other_currency_count": 0 },
    "by_owner": [ { "owner_id": "4a3c…", "name": "Rex Ortega", "email": "rex@acme.com", "won": 5, "lost": 4, "won_amount": 72000, "win_rate": 0.5556 } ],
    "by_period": [ { "period": "2026-07-01", "won": 4, "lost": 7, "won_amount": 51000, "win_rate": 0.3636 } ],
    "range": { "from": "2026-07-01T04:00:00Z", "to": "2026-10-01T04:00:00Z", "time_zone": "America/New_York", "interval": "week" },
    "currency": "USD",
    "fiscal_year_start_month": 1,
    "period": { "preset": "last_quarter", "start_date": "2026-07-01", "end_date": "2026-09-30" }
  },
  "previous": { "totals": { "won": 9, "lost": 17, "win_rate": 0.3462 }, "period": { "start_date": "2026-04-01", "end_date": "2026-06-30" } }
}
```

With [deal splits](https://docs.useorbitcrm.ai/developers/api/crm.md#deal-splits) on, `win_rate` also carries `split_credit`: `{ "split_deal_count": 3, "by_person": [{ "user_id": "4a3c…", "name": "Rex Ortega", "won_amount": 43200, "won_deals": 2.6, "split_deals": 1 }] }`, the same won deals credited by their splits (a deal without splits credits its owner). The person filter picks credited people. Revenue won goals use it.

## Definitions

Amounts are in the workspace currency (`default_currency` in deal settings): deals in another currency are converted at the workspace's exchange rates (see [`GET /api/v1/currencies`](https://docs.useorbitcrm.ai/developers/api/crm#currencies-and-exchange-rates)), won and lost deals at the rate they closed at. A deal whose currency has no rate is counted, its amount left out and counted in `other_currency_count`. Rates are fractions (`0.4` is 40%), `null` when there is nothing to divide by. Durations are seconds.

| Report | What it measures |
| - | - |
| `pipeline` | Open deals right now, per open stage: `deal_count`, `amount`, and `weighted_amount` (amount × the stage's probability ÷ 100). A snapshot: the date range doesn't apply |
| `win_rate` | Deals closed in the period, by the day they closed: won ÷ (won + lost). Also per owner and per period, the average won amount, and the average of each won deal's whole days from creation to close |
| `conversion` | Per open stage: the deals that **first** entered it in the period (`entered`), how many of them later entered the next stage (`to_next`; after the last open stage, the won stage) and how many were later won (`to_won`, in a won stage of the same pipeline). Re-entering a stage never counts a deal twice; skipping the next stage doesn't count as moving to it |
| `stage_time` | The same deals per stage, and the time each spent in it, every visit added up (the time the deal page shows): `average_seconds`, `median_seconds`, and `in_stage_now` (still there; their time counts to now) |
| `deal_flow` | Deals created in the period and deals won in it, per period and per `source` (`manual`, `workflow`, `import`, `api`, and `renewal` for deals the auto-renewal created, once there are some) |
| `activity` | Per rep: emails sent (one-to-one, sequence, and logged or synced outbound emails), calls (dialer calls that connected, were busy or unanswered, and logged calls), meetings (booked meetings that have started, and logged meetings) and tasks completed. Only activity on records the credential owner can open counts. Email synced from Gmail or Outlook follows the same visibility rule as the [activities API](https://docs.useorbitcrm.ai/developers/api/crm.md) |
| `renewals` | Won deals whose `renewal_date` falls in the period, **by month** (`range.interval` is always `month`): `due`, `mrr` and `arr` (their recurring amount per month, a yearly amount counting as a twelfth, and twelve months of it), and what became of each renewal: `renewed` (a renewal deal for it was won), `open`, `lost`, or `not_started`. A renewal deal is one the auto-renewal created for it (`renewed_from_deal_id`). `renewal_rate` is renewed ÷ (renewed + lost). Also `by_period` and `by_owner` |
| `recurring` | Won deals with a `recurring_amount`, as `deals`, `mrr` and `arr` in three measures: `won` (won in the period, by the day they closed), `run_rate` (in force now, a snapshot the range doesn't change: no renewal date or one still to come, and not replaced by a won renewal deal) and `lapsed` (the renewal date passed with no won renewal deal). `by_type` splits won and in force by `deal_type` (`new_business`, `expansion`, `renewal`, `none`); `by_period` has the MRR won per period |
| `response` | Contacts created in the period (imports left out), from creation to the first outreach by a person: a one-to-one email (including one an agent sends as a person through the API or MCP), a dialer call, or a logged or synced outbound email, call or text (automated sequence and workflow sends don't count; synced email follows the activities API's visibility rule). Per owner: `leads`, `contacted`, `not_contacted`, `median_seconds`, `average_seconds` |

## Rows behind a number

Pass the report's filters plus what the number is about:

| Report | Parameters |
| - | - |
| `pipeline` | `stage_id`, `owner` |
| `win_rate` | `status` (`won` or `lost`), `owner`, `period_start` |
| `deal_flow` | `measure` (`created` or `won`, required), `period_start`, `source`, `owner` |
| `conversion` | `stage_id`, `outcome` (`next` or `won`), `owner` |
| `stage_time` | `stage_id`, `state` (`in_stage` or `moved_on`), `owner` |
| `activity` | `kind` (`email`, `call`, `meeting` or `task`), `owner` |
| `response` | `state` (`contacted` or `waiting`), `owner` |
| `renewals` | `period_start` (a month), `state` (`renewed`, `open`, `lost` or `none`), `owner`. Rows list the soonest renewal first and add `deal_type`, `recurring_amount`, `recurring_interval`, `renewal_date` and `renewal_state` |
| `recurring` | `measure` (`won`, `run_rate` or `lapsed`, required), `deal_type` (`new_business`, `expansion`, `renewal` or `none`), `period_start`, `owner`. Rows add `deal_type`, `recurring_amount`, `recurring_interval` and `renewal_date` |

`owner` is a user ID, or `none` for records without one. `period_start` is a period's first date as the report lists it in `by_period`. A linked contact, company or deal is only named in an activity row when the credential owner can open it and the credential also holds `contacts:read`, `companies:read` or `deals:read` respectively.

## Goals and attainment

Admins set goals in OrbitForms (**Analytics › Goals**) per person or team (member group), for a month or a quarter of the fiscal year, on four metrics. Each actual is a number the Sales dashboard already shows, from the same calculation, so attainment always matches the reports for the same period:

| `metric` | Actual |
| - | - |
| `revenue_won` | `win_rate`'s won amount: deals won in the period, by the day they closed, in the workspace currency |
| `deals_won` | `win_rate`'s won deals |
| `calls` | `activity`'s calls, by whoever made them |
| `meetings` | `activity`'s meetings, by their host |

`GET /api/v1/goals` takes the [period filters](#filters) (`period`, or `from` and `to`, and `time_zone`), plus `metric` (default all four), `owner_id` (a user ID or `me`) or `group_id` to narrow who is listed, and `order` (`attainment`, the default, or `actual`). `pipeline_id` doesn't apply: goals are for all pipelines. It needs `deals:read`; `calls` and `meetings` also need `activities:read`, and are listed in `omitted_metrics` without it.

Over a range, a person's goal adds up their goals for it: per fiscal quarter the range covers whole, its quarterly goal (or, without one, its months' goals), plus the monthly goals of the other months. A range that isn't whole months (`last_7_days`, say) has `aligned: false` and no goals. A team's actual is the sum of its members' now. `attainment` is actual ÷ goal, `null` without a goal.

```json theme={null}
{
  "data": {
    "period": { "preset": "this_quarter", "start_date": "2026-10-01", "end_date": "2026-12-31", "time_zone": "UTC" },
    "currency": "USD",
    "aligned": true,
    "visibility": "everyone",
    "is_manager": false,
    "metrics_with_goals": ["revenue_won"],
    "goals": [ { "id": "1f0c…", "metric": "revenue_won", "period_type": "quarter", "period_start": "2026-10-01", "user_id": "4a3c…", "group_id": null, "target": 90000 } ],
    "attainment": [
      {
        "metric": "revenue_won",
        "team": { "actual": 72000, "target": 90000, "attainment": 0.8, "people_with_goal": 1 },
        "reps": [ { "user_id": "4a3c…", "name": "Rex Ortega", "email": "rex@acme.com", "actual": 72000, "target": 90000, "attainment": 0.8, "rank": 1 } ],
        "groups": [ { "group_id": "9b2e…", "name": "East", "member_count": 3, "actual": 140000, "target": null, "attainment": null } ]
      }
    ],
    "omitted_metrics": []
  }
}
```

People and teams follow the credential owner's record access (a rep who sees only their own deals sees only their own goals), and the workspace's goals visibility setting (Settings › Deals): with `admins`, everyone but admins sees only their own. A team is listed when everyone in it is.

## Forecast

`GET /api/v1/forecast` (`deals:read`) answers the forecast for one month or fiscal quarter: `period` (`this_month`, `next_month`, `last_month`, `this_quarter` (default), `next_quarter` or `last_quarter`), or `period_type` (`month` or `quarter`) with `period_start` (its first day; a quarter must start a fiscal quarter). Also `pipeline_id`, `owner_id`, `group_id` and `time_zone`.

Every deal counts in one **forecast category**, the deal's `forecast_category` in the [deals API](https://docs.useorbitcrm.ai/developers/api/crm.md#deals): `closed` (won), `commit`, `best_case`, `pipeline` or `omitted` (lost, or left out). An open deal's category comes from its stage (set per stage in Settings › Pipelines; until then by probability: 80% or more is `commit`, 50% or more `best_case`, else `pipeline`), unless it was set on the deal, which lasts until the deal moves stage.

| Field | What it is |
| - | - |
| `closed` | Deals won in the period, by the day they closed: `win_rate`'s won amount for the same filters |
| `commit`, `best_case`, `pipeline`, `omitted` | Open deals whose expected close date (`close_date`) is in the period, by category (each with a `_count`) |
| `weighted` | Those open deals except omitted ones, amount × the stage's probability ÷ 100 |
| `undated_count` | Open deals with no close date, so in no period |
| `past_due_count` | Open deals whose close date passed before the period (and before today), so in no forecast until the date moves |
| `system` | `closed` + `commit` |
| `goal` | The person's or team's `revenue_won` goal for the period |
| `submitted` | The person's latest forecast for the period (`amount`, `created_at`, `note`) |
| `override` | An admin's latest number for the person or team, with a `note`; `null` once cleared |
| `accuracy` | Once the period is over: 1 − \|submitted − closed\| ÷ closed (`override_accuracy` for the override). A team's needs every member's forecast |

`can_submit` says whether the credential owner may submit their own forecast (members who can make changes; not the viewer role). `rows` has one entry per person (every member the credential owner may see, zero or not, plus deals with no owner as `owner_id: null`), `groups` one per team (its members added up; `submitted_amount` is the sum of their submissions, `submitted_count` how many submitted), and `submissions` every submission and override for the period and pipeline scope (`pipeline_id`, or all pipelines), newest first: numbers are never changed or removed. Submissions are made in OrbitForms (**Reports › Forecast**) until the period's last day; the API reads them.

## Ask a question

`POST /api/v1/reports/questions` answers a reporting question in plain English, the same as the question box on the Analytics page:

```json theme={null}
{ "question": "Win rate by source last quarter", "time_zone": "Europe/London" }
```

Orbit's AI plans the report from the workspace's field, list and people names only: it never sees a record and never writes a query. Orbit checks the plan against a fixed list of objects (`deals`, `contacts`, `companies`, `tasks`, `activities`), measures (`count`, `sum`, `avg`, `min`, `max`, `median`, `win_rate`), fields and filters, then runs it on read-only reporting views scoped to the workspace and the credential owner's record access. A question reads at most 250,000 records and answers at most 200 rows.

```json theme={null}
{
  "data": {
    "status": "answered",
    "title": "Win rate by source, last quarter",
    "assumptions": ["Source means how the deal was created."],
    "spec": { "object": "deals", "measures": [{ "op": "win_rate", "field": null }], "group_by": [{ "field": "source", "bucket": null }], "filters": [], "date": { "field": "closed_at", "period": "last_quarter", "from": null, "to": null }, "sort": "value_desc", "limit": 25, "version": 1 },
    "answer": {
      "groups": [{ "field": "source", "label": "Source", "bucket": null }],
      "measures": [{ "label": "Win rate", "format": "rate" }],
      "rows": [{ "keys": ["import"], "labels": ["Import"], "values": [0.75] }],
      "display": "table",
      "period": { "start_date": "2026-07-01", "end_date": "2026-09-30", "label": "Last quarter" },
      "explanation": ["Shows win rate of deals.", "Deals closed from Jul 1, 2026 to Sep 30, 2026 (last quarter).", "…"],
      "currency": "USD"
    },
    "credits_used": 1
  }
}
```

`explanation` says what was counted, worked out from the spec rather than written by the AI. A question that isn't about this data answers `{ "status": "unanswerable", "message": "…" }` and uses no credit. The answer's object needs its read scope (`deals:read`, `contacts:read`, `companies:read`, `tasks:read` or `activities:read`); without it the request is refused (403) and nothing is charged. Rates are fractions, amounts are in the workspace's default deal currency. Limits are shared with record summaries: 40 AI requests per person per 10 minutes and 200 paid AI actions per workspace per day (429); 402 when the workspace is out of AI credits; 503 when the AI is unavailable. Each question is kept in the asker's own question list on the Analytics page.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.