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

# Email stats rollup

> 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.




## OpenAPI

````yaml /api-reference/openapi.yaml get /v2/emails/stats
openapi: 3.1.0
info:
  title: Inflection Developer 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: Contacts
    description: Create, read, and update contacts. Writes are asynchronous.
  - name: Contact Activity
    description: >-
      Product, marketing, and combined activity for a contact, plus Salesforce
      data.
  - name: Journeys
    description: Read journeys (campaigns), their schedule and their derived status.
  - name: Stats
    description: >-
      Send and engagement metrics for journeys and emails, with a journey
      rollup.
  - name: Lists
    description: Static and dynamic lists, and static-list members.
  - name: Emails
    description: Email listing and creation.
  - name: Product Users
    description: |
      Read product users -- the logins underneath a contact -- by id or email,
      and list them for a contact or an organization.
  - name: Organizations
    description: Read organizations (company accounts) and a contact's memberships.
  - name: Email Versions
    description: Per-contact versions of a Personalized Email Asset, served under `/v1`.
paths:
  /v2/emails/stats:
    get:
      tags:
        - Stats
      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.
      operationId: getEmailsStatsRollup
      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'
components:
  parameters:
    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
    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
    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
    PageNumber:
      name: pageNumber
      in: query
      required: false
      description: 1-based page number.
      schema:
        type: integer
        minimum: 1
        maximum: 2147483647
        default: 1
    PageSize:
      name: pageSize
      in: query
      required: false
      description: Items per page.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 20
  schemas:
    EmailStatsRollupResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/EmailStatsRollup'
        pagination:
          $ref: '#/components/schemas/Pagination'
        meta:
          $ref: '#/components/schemas/Meta'
    ErrorEnvelope:
      type: object
      description: Standard failure envelope.
      properties:
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ErrorItem'
        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'
    Pagination:
      type: object
      description: Present only on paged list responses.
      properties:
        pageNumber:
          type: integer
        pageSize:
          type: integer
        totalElements:
          type: integer
        totalPages:
          type: integer
    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
    ErrorItem:
      type: object
      properties:
        errorCode:
          type: string
          examples:
            - NOT_FOUND
        message:
          type: string
        detail:
          type: string
      required:
        - errorCode
        - message
    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
    EmailStatsRow:
      description: One email's counters within a rollup page.
      allOf:
        - type: object
          properties:
            templateId:
              type: string
        - $ref: '#/components/schemas/Stats'
  responses:
    Unauthorized:
      description: Missing or invalid Personal Access Token. Empty body.
    Forbidden:
      description: >-
        The PAT lacks the required scope (READ for reads, WRITE for writes).
        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
  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`.

````