Stats a campaign

GET/api/v1/campaigns/{campaign}/stats
Requires anorganization API keywith the scopecampaigns:read

Parameters

NameInTypeDescription
campaignrequiredpathstringThe campaign identifier.

Request

GET/api/v1/campaigns/{campaign}/stats
curl -X GET 'https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/stats' \
  -H 'Authorization: Bearer mk_live_YOUR_API_KEY'
The key names its own organization, so no X-Organization-ID header is needed.

Response

Success.

  • dataobject

    No rates are published: you have both inputs, and a published percentage bakes in a denominator and a rounding rule we could never change. CORRELATION CAVEAT: only events tagged `campaign:{id}` are exact. Opens, clicks, bounces and unsubscribes arrive from the tracking pipeline without that tag and are matched by recipient address within the campaign's active window, so an address on two overlapping campaigns can have one open counted against both. Counts are unique by recipient.

    • objectstringcampaign_stats
    • campaign_idstring
    • statestringdraft | scheduled | sending | paused | sent | canceled | unknown

      The campaign's lifecycle state, repeated from the same map `campaign.state` uses so the numbers can be read without a second request. Note that `sent` describes the fan-out finishing, not the numbers below: a campaign can read `sent` with `recipients.sent: 0`.

    • recipientsobject

      What the fan-out did with the ledger. EVERY count in this group is `null` until the campaign starts, and an integer from the moment it does. The cut is exact, not a guess: the recipient ledger is materialised and the measurement window opens in the same save that sets `schedule.started_at`, so `started_at === null` is precisely "no ledger, no window, nothing measured". Null here means UNKNOWN, never zero — "nobody opened it" and "it has not been sent" are different facts.

      • totalinteger

        Every row in the recipient ledger. Null until the campaign starts — see the group description.

      • pendinginteger

        Materialised, not yet attempted. Null until the campaign starts — see the group description.

      • sentinteger

        Handed to the mail server. NOT a delivery confirmation — that is `engagement.delivered`, and the two are kept separate because a campaign with `sent` above `delivered` has mail in flight or bounced. Null until the campaign starts — see the group description.

      • failedinteger

        The send attempt threw. Null until the campaign starts — see the group description.

      • skipped_suppressedinteger

        On the organization's suppression list. Null until the campaign starts — see the group description.

      • skipped_unsubscribedinteger

        Opted out of this organization's mail. Null until the campaign starts — see the group description.

    • engagementobject

      What came back. EVERY count in this group is `null` until the campaign starts, and an integer from the moment it does. The cut is exact, not a guess: the recipient ledger is materialised and the measurement window opens in the same save that sets `schedule.started_at`, so `started_at === null` is precisely "no ledger, no window, nothing measured". Null here means UNKNOWN, never zero — "nobody opened it" and "it has not been sent" are different facts.

      • deliveredinteger

        Confirmations received from the receiving server. Null until the campaign starts — see the group description.

      • openedinteger

        Unique recipients who opened. Null until the campaign starts — see the group description.

      • clickedinteger

        Unique recipients who clicked. Null until the campaign starts — see the group description.

      • bouncedinteger

        Unique recipients whose message bounced. Null until the campaign starts — see the group description.

      • unsubscribedinteger

        Unique recipients who opted out from this message. Null until the campaign starts — see the group description.

    • generated_atstring

      When this rollup was computed. The endpoint recomputes on every call rather than reading the campaign's cached copy, so it is always "just now" — published anyway, because a caller storing the answer needs to know how old their copy is. In practice NEVER null: the service stamps it on every rollup it returns. The marker is defensive, and a null would mean UNKNOWN — the rollup reached us without a timestamp — never "it has not been computed", because the counts beside it would not exist if it had not been.

Returned inside the standard envelope.

Errors

StatusWhen
401The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist.
403The key is valid but may not do this: it lacks the required scope, its IP allowlist does not include you, or this endpoint does not accept API keys.
404No such resource in this organization.
422The request was understood but the values were not acceptable.
429Too many requests, or the organization has spent its sending allowance. `Retry-After` says how long to wait.

Every status, with what causes it and what to do, is on the error reference.