Inflection.io Stats API
Send and engagement metrics for journeys and emails, with a journey rollup.
Send and engagement metrics for journeys and emails, with a journey rollup.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/inflectionio:inflectionio-stats-api"
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Inflection Developer Stats API
version: 1.0.0
description: 'The Inflection Developer API (`/v2`). Authenticate with a Personal Access
Token (`Authorization: Bearer inf_pat_...`).
**`/v2` is `/v1` plus more.** Every `/v1` endpoint is served here unchanged,
so a client can pin to one version and reach the whole API. On top of those
sit reads `/v1` never had: journeys, journey and email stats with a journey
rollup, and the email and list collections. `/v1` remains published.
**Response envelope.** Every response is wrapped:
`{ "data": …, "pagination": …, "errors": [...], "meta": { "status": "SUCCESS|FAILURE", "timestamp": "…" } }`.
`data` carries the payload, `pagination` appears only on paged lists, and on
failures `errors` is populated with `meta.status: "FAILURE"`.
**Query parameters are camelCase**: `pageNumber`, `pageSize`, `startTime`,
`folderId`. The journeys, emails, lists and stats reads additionally accept
the snake_case spelling of every scalar parameter (`page_number`,
`folder_id`, …); the contact, activity and list-member endpoints do not.
Repeatable parameters also accept a `[]` suffix (`status[]`, `tag[]`).
**Asynchronous contact writes.** `POST/PATCH /v2/contacts*` are processed
asynchronously: they return **200** with a `PENDING` transaction
acknowledgement (not the created/updated contact). Poll
`GET /v2/contacts/transactions/{transactionId}` for the per-contact result.
**Status-code quirks** (documented per-operation): a missing **contact** is
reported as **400** (not 404), whereas a missing **list**, **email** or
**journey** is **404**. For journeys and emails that also covers a
soft-deleted record and a malformed id. Nothing returns 201 or 204.
**Empty results are not errors.** On the paged reads, a `tag`, `folderId` or
`search` that matches nothing returns an empty page, not a 404.
**Errors.** Failures use the envelope above
(`errors[].{errorCode,message,detail}`). Authentication and authorization
failures (401/403) are returned with an empty body. The journey stats rollup
adds `RESULT_SET_TOO_LARGE` (a **400**) when a filter matches more journeys
than one request may cover; narrow it rather than paging further.
**Rate limiting.** Requests are metered per tenant. Exceeding the budget
yields **429** (empty body) on any endpoint; the `Retry-After` header gives
the seconds to wait and `X-RateLimit-Limit` the sustained per-second budget.'
servers:
- url: https://api.inflection.io
description: Production
security:
- PatAuth: []
tags:
- name: Stats
description: Send and engagement metrics for journeys and emails, with a journey rollup.
paths:
/v2/journeys/stats:
get:
tags:
- Stats
operationId: getJourneysStatsRollup
summary: Journey stats rollup
description: 'Counters for many journeys in one call, so reporting on 50 journeys is
one request rather than 50.
`data.aggregate` is a single ungrouped query over the **whole** matching
set, so it is **not** the sum of `data.records`. Those cover only the
current page. `pagination.totalElements` counts the whole matching set.
At most **one** of `ids`, `tag`, `folderId` may be given; more than one
is a 400. Explicit `ids` return a full row of zeros for a journey with no
activity, whereas a `tag`/`folderId`/unfiltered request returns only
journeys that have activity. A filter matching more than 1000 journeys is
a 400 (`RESULT_SET_TOO_LARGE`). Nothing is silently trimmed. That guard
cannot trigger when `ids` is used, since `ids` is itself capped at 50.'
parameters:
- $ref: '#/components/parameters/RollupIds'
- $ref: '#/components/parameters/TagName'
- $ref: '#/components/parameters/FolderId'
- $ref: '#/components/parameters/Since'
- $ref: '#/components/parameters/Until'
- $ref: '#/components/parameters/PageNumber'
- $ref: '#/components/parameters/PageSize'
responses:
'200':
description: The aggregate plus one row per journey on the page.
content:
application/json:
schema:
$ref: '#/components/schemas/StatsRollupResponse'
'400':
description: '`VALIDATION_ERROR` for a malformed date, more than one narrowing
filter, or more than 50 `ids`; or `RESULT_SET_TOO_LARGE` when the
filter matches more than 1000 journeys.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/TooManyRequests'
/v2/journeys/{id}/stats:
get:
tags:
- Stats
operationId: getJourneyStats
summary: Get a journey's stats
description: 'The thirteen counters for one journey. `since`/`until` bound the **send**
time; omitting both covers all time.'
parameters:
- $ref: '#/components/parameters/JourneyId'
- $ref: '#/components/parameters/Since'
- $ref: '#/components/parameters/Until'
responses:
'200':
description: The journey's stats. Not paged.
content:
application/json:
schema:
$ref: '#/components/schemas/StatsResponse'
'400':
description: A malformed `since` or `until`; `detail` names the field.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: Unknown, soft-deleted, or malformed journey id.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/TooManyRequests'
/v2/emails/stats:
get:
tags:
- Stats
operationId: getEmailsStatsRollup
summary: Email stats rollup
description: 'Counters for many emails in one call. An email reused across journeys is
reported once here, on its own totals. Per-journey stats cannot answer
"how is this email performing" when the caller does not know which
journeys send it.
`data.aggregate` is a single ungrouped query over the **whole** matching
set, so it is **not** the sum of `data.records`. Those cover only the
current page.
It is also **not** the sum of every page. A send that carries two
different templates counts once in `data.aggregate` but once per template
in `data.records`, so summing all pages can exceed the aggregate by a
small margin (1 in 27,613 on our staging data). The aggregate is the
de-duplicated figure; prefer it over adding the rows up.
Emails are matched on **send** activity. An email with opens or clicks
but no send record is not reported, and its engagement is not in the
aggregate either.
At most **one** of `ids`, `folderId` may be given; both is a 400.
Explicit `ids` return a full row of zeros for an email with no activity,
whereas a `folderId`/unfiltered request returns only emails that have
activity. A filter matching more than 1000 emails is a 400
(`RESULT_SET_TOO_LARGE`). Nothing is silently trimmed. That guard cannot
trigger when `ids` is used, since `ids` is itself capped at 50.
An email whose template record no longer exists is still reported when it
has send activity. The sends happened, and the tracking is the record of
them. Those ids are therefore reported here but return 404 from
`GET /v2/emails/{id}/stats`. This is deliberately unlike the journey
rollup, which reports only live journeys.'
parameters:
- $ref: '#/components/parameters/EmailRollupIds'
- $ref: '#/components/parameters/FolderId'
- $ref: '#/components/parameters/Since'
- $ref: '#/components/parameters/Until'
- $ref: '#/components/parameters/PageNumber'
- $ref: '#/components/parameters/PageSize'
responses:
'200':
description: The aggregate plus one row per email on the page.
content:
application/json:
schema:
$ref: '#/components/schemas/EmailStatsRollupResponse'
'400':
description: '`VALIDATION_ERROR` for a malformed date, both narrowing filters at
once, or more than 50 `ids`; or `RESULT_SET_TOO_LARGE` when the
filter matches more than 1000 emails.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/TooManyRequests'
/v2/emails/{id}/stats:
get:
tags:
- Stats
operationId: getEmailStats
summary: Get an email's stats
description: 'The same thirteen counters as journey stats, aggregated across **every**
journey that sends this email.'
parameters:
- $ref: '#/components/parameters/EmailId'
- $ref: '#/components/parameters/Since'
- $ref: '#/components/parameters/Until'
responses:
'200':
description: The email's stats. Not paged.
content:
application/json:
schema:
$ref: '#/components/schemas/StatsResponse'
'400':
description: A malformed `since` or `until`; `detail` names the field.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: Unknown or malformed email id.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/TooManyRequests'
components:
responses:
Unauthorized:
description: Missing or invalid Personal Access Token. Empty body.
TooManyRequests:
description: 'Per-tenant rate limit exceeded. Empty body. Retry after the number of
seconds given in the `Retry-After` header.
'
headers:
Retry-After:
description: Seconds to wait before retrying.
schema:
type: integer
X-RateLimit-Limit:
description: Sustained request budget, in requests per second, per tenant.
schema:
type: integer
Forbidden:
description: The PAT lacks the required scope (READ for reads, WRITE for writes). Empty body.
schemas:
StatsRollupResponse:
type: object
properties:
data:
$ref: '#/components/schemas/StatsRollup'
pagination:
$ref: '#/components/schemas/Pagination'
meta:
$ref: '#/components/schemas/Meta'
ErrorItem:
type: object
properties:
errorCode:
type: string
examples:
- NOT_FOUND
message:
type: string
detail:
type: string
required:
- errorCode
- message
StatsRollup:
type: object
properties:
aggregate:
allOf:
- $ref: '#/components/schemas/Stats'
description: 'One ungrouped query over the **whole** matching set, deliberately
not the sum of `records`, which covers only the current page. All
zeros when nothing matched.
'
records:
type: array
items:
$ref: '#/components/schemas/JourneyStatsRow'
Meta:
type: object
description: Envelope metadata present on every response.
properties:
status:
type: string
enum:
- SUCCESS
- FAILURE
timestamp:
type: string
format: date-time
required:
- status
StatsResponse:
type: object
properties:
data:
$ref: '#/components/schemas/Stats'
meta:
$ref: '#/components/schemas/Meta'
EmailStatsRollup:
type: object
properties:
aggregate:
allOf:
- $ref: '#/components/schemas/Stats'
description: 'Totals for every matching email, across all pages, deliberately not
the sum of `records`, which covers only the current page. All zeros
when nothing matched.
'
records:
type: array
items:
$ref: '#/components/schemas/EmailStatsRow'
EmailStatsRow:
description: One email's counters within a rollup page.
allOf:
- type: object
properties:
templateId:
type: string
- $ref: '#/components/schemas/Stats'
Stats:
type: object
description: 'The thirteen send and engagement counters. Every field is always present
and defaults to `0`. None are null or omitted.
'
properties:
totalCount:
type: integer
description: Emails sent.
notSentCount:
type: integer
processedCount:
type: integer
deliveredCount:
type: integer
droppedCount:
type: integer
bounceCount:
type: integer
spamreportCount:
type: integer
totalOpenCount:
type: integer
uniqueOpenCount:
type: integer
totalClickCount:
type: integer
uniqueClickCountByUrl:
type: integer
uniqueClickCountByEmail:
type: integer
unsubCount:
type: integer
Pagination:
type: object
description: Present only on paged list responses.
properties:
pageNumber:
type: integer
pageSize:
type: integer
totalElements:
type: integer
totalPages:
type: integer
JourneyStatsRow:
description: One journey's counters within a rollup page.
allOf:
- type: object
properties:
journeyId:
type: string
- $ref: '#/components/schemas/Stats'
ErrorEnvelope:
type: object
description: Standard failure envelope.
properties:
errors:
type: array
items:
$ref: '#/components/schemas/ErrorItem'
meta:
$ref: '#/components/schemas/Meta'
EmailStatsRollupResponse:
type: object
properties:
data:
$ref: '#/components/schemas/EmailStatsRollup'
pagination:
$ref: '#/components/schemas/Pagination'
meta:
$ref: '#/components/schemas/Meta'
parameters:
JourneyId:
name: id
in: path
required: true
description: Journey (campaign) id.
schema:
type: string
PageSize:
name: pageSize
in: query
required: false
description: Items per page.
schema:
type: integer
minimum: 1
maximum: 200
default: 20
FolderId:
name: folderId
in: query
required: false
description: 'Restrict to one folder. An id matching no folder yields an empty page
rather than an error.
'
schema:
type: string
Since:
name: since
in: query
required: false
description: 'ISO-8601 lower bound on the **send** time, inclusive. A date
(`2026-01-01`), a `Z` offset and a `±HH:MM` offset are all accepted and
normalised to UTC; a value carrying no offset is read in the server''s
zone. Omit for all time.
'
schema:
type: string
TagName:
name: tag
in: query
required: false
description: 'Repeatable. Matches the tag **name**, not its id. A name matching no tag
yields an empty page.
'
schema:
type: array
items:
type: string
EmailRollupIds:
name: ids
in: query
required: false
description: 'Email ids to report on, at most **50** (a URL-length limit). Mutually
exclusive with `folderId`.
'
schema:
type: array
maxItems: 50
items:
type: string
PageNumber:
name: pageNumber
in: query
required: false
description: 1-based page number.
schema:
type: integer
minimum: 1
maximum: 2147483647
default: 1
RollupIds:
name: ids
in: query
required: false
description: 'Journey ids to report on, at most **50** (a URL-length limit). Mutually
exclusive with `tag` and `folderId`.
'
schema:
type: array
maxItems: 50
items:
type: string
Until:
name: until
in: query
required: false
description: 'ISO-8601 upper bound on the send time, inclusive. Same accepted forms as
`since`.
'
schema:
type: string
EmailId:
name: id
in: path
required: true
description: Email id.
schema:
type: string
securitySchemes:
PatAuth:
type: http
scheme: bearer
bearerFormat: inf_pat
description: 'Personal Access Token or OAuth 2.1 access token, sent as a bearer
credential (`Authorization: Bearer inf_pat_...`). Read operations
require a token with the `READ` scope; write operations
(POST/PATCH/DELETE) require `WRITE`.
'
x-provenance:
method: harvested
publisher: Inflection.io
source: https://docs.inflection.io/api-reference/openapi.yaml
note: split by refine-openapis from the provider's own published document, verified by fetch (openapi/_original/*.fetch.json);
stamped by stamp-openapi-provenance.py (roadmap#793)