Guides

How Curviate counts

Every number the Analytics reads return, and the rule behind it. Every figure on this page is the one the API computes with.

Analytics is read-only: GET /v1/analytics/motions, GET /v1/analytics/motions/{motion_id}, GET /v1/analytics/activity and GET /v1/analytics/events, or get_analytics over MCP. A read makes no LinkedIn call and spends no limit, and it answers on every Tier. Every number is computed when you ask; nothing is cached.

Two rates, never a score

Acceptance and replies measure different things, and which one matters depends on your goal. Curviate returns both side by side, each with its numerator, its denominator and low_sample, and never blends them into a score, a rank or a winner.

Every rate reads the same way: of what was sent in the period, the share accepted or replied so far. Numerator and denominator are the same people, whatever the period's length.

  • Acceptance: of the invitations sent in the period, those accepted so far, whenever detected, even after the Enrollment ended. Withdrawn invitations stay in the denominator. A re-sent invitation is a second send; an acceptance counts for the latest invitation sent before it. An invitation whose send result was unknown counts as sent once it is accepted.
  • Reply: of the Enrollments first messaged in the period with no reply before that message, those who replied at or after it. A message sent after a reply (an answer, not outreach) never enters the denominator.
  • InMail: counted apart. inmails holds InMails sent and the people first InMailed, and inmail_reply is the reply rate over them, by the same rule. messages and reply never include InMail. One reply after both a message and an InMail counts in both reply rates.
  • Pending: pending is the part of the denominator with no outcome yet that can still come: an invitation neither accepted nor withdrawn; a messaged Enrollment that is still open, or finished at most 30 days ago (a reply up to 30 days after the Enrollment finished still counts).
  • Still maturing: maturing is the part of the denominator sent less than 7 days ago, and maturing_share its share. The sends stay in the rate; when more than a third of the denominator is maturing, the rate is still moving.
  • Small samples: low_sample is true when the denominator is below 30. The rate is still returned. A denominator of 0 gives rate: null.

The acceptance floor on a Motion is optional and off by default. When you turn it on, it keeps its own rule: it counts invitations once they are 7 days old.

What counts as a reply

A reply is an inbound message in a 1:1 chat on the Enrollment's account from the enrolled person, counted once per Enrollment, at the first one. Three messages from one person are one reply. Any other inbound message (another chat, a group chat, the same person writing to another of your accounts) is not counted: Analytics never counts "any inbound". A reply up to 30 days after the Enrollment finished is still its reply, recorded without restarting anything and without a webhook.

Per step, a reply counts for the message step it followed, before the next message step (last touch).

Seen

A message step's seen counts its executions in the period that were seen so far: a read receipt arrived for that message, or the person replied after it (a reply proves it was read; this one is not last touch). InMail counts the same way. People can turn read receipts off, so the seen rate is a floor. A receipt says a message was seen, never when it was read: Curviate records when the receipt arrived. GET /v1/enrollments?done_step=<step>&outcome=seen lists the same Enrollments by the same rule.

Reasons

Each step row carries reasons: its skipped and failed executions counted by reason code, and the stats block carries enrollment_reasons: skipped Enrollments by skip_reason, failed ones by finish_reason. Each map adds up to its count, and a code with zero is left out. Every code, with its sentence and what to do about it, is on Motion reasons. The events feed returns the same sentence as reason_message. A wait for an event that times out is no reason: it counts in else.

Variants and the fallback

A step with two or more variants has one row per variant, in document order and never sorted by result. When the step has a fallback that sends something, a last row with fallback: true counts the sends that used the fallback text, whatever variant was assigned. Those sends also stay in their variant's executed and fallback_used; own_text_executed is the part that sent the variant's own text. Curviate never names a winner.

Paths

The one-Motion read splits its numbers into three paths by how the Contact related to the account when the Enrollment started: connected, not_connected (a pending invitation included) and unknown (the account's Connections are not synced). Only started Enrollments are in a path. This split is provisional.

Who a Motion read counts

The one-Motion read takes variant_id (the Enrollments given that variant) and started_from and started_to (the Enrollments started on those days, read in timezone). Every figure then counts only those Enrollments, whenever their activity happened: totals, steps, paths, accounts. Queued Enrollments have not started and never count. scope echoes the filters.

Daily counts have no Enrollment. So such a read takes nothing from them: counts_only_through and paths_through are null, and when the window reaches them window.cohort_after names the day an Enrollment must have started after to count. It is null otherwise.

accept_time, on the read and on each path, holds median_seconds from an invitation's send to when its acceptance was detected, and accepted, the acceptances that median is over. It reads full detail only, so past counts_only_through it covers the later days.

Periods, filters and grouping

Pick the period with range (7d, 30d, 90d, 12m or all) or with from and to, never both: together they answer 400. A period ends today in timezone. 12m is the same date a year back, so it covers 366 days across a 29 February. all starts on the day your Team was created. Without either, the window is the last 30 days.

A custom window of the Motion reads and the Account overview is at most 3,650 days, and may start before your Team existed (those days read zero). Over 3,650 days answers 400.

Past 12 months

Curviate keeps the full detail of every step for 12 months. Older days are kept as daily counts, so a window that reaches back further still answers, with less detail on the oldest part:

  • window.counts_only_through is the last day read from daily counts (YYYY-MM-DD), or null when every day is read in full detail. Up to that day, totals and daily hold the counts, as UTC days whatever the timezone. Step and variant rates, seen and paths do not cover those days.
  • window.paths_through is that same day. paths covers the days after it only, so past it paths does not add up to totals. Do not compare them across that date.
  • The 12m range never reaches daily counts, in any timezone. all does, for a Team older than a year.
  • The events feed and an Enrollment's history reach back 12 months, and history_truncated is true on an Enrollment that started before that. The Enrollment's state and the Enrollment list have no such limit.

daily is grouped by bucket: day, week (starting Monday) or month (starting on the 1st). auto uses day up to 90 days, week up to 366 days and month beyond. It is the default with range; with from and to the default is day. window.bucket tells you which one was used, and each entry's date is its bucket's first day.

The Motions overview also filters by motion_id and account_id (repeat the key or separate with commas) and by origin: motion for what Motions sent, unibox for what you sent from the Unibox, api for what any API key or MCP grant sent (REST, CLI, MCP, SDK), and direct for both of the last two. Direct actions count in the totals, the daily series and the account rows, never in a Motion's row; their outcomes count for the account and the person they were sent to. Each Contact counts once per Account and source. origin=linkedin is refused: actions taken on LinkedIn itself are never counted.

Units

Enrollment states, cohorts and replies count Enrollments. Each Contact counts once per Account. A person enrolled with two accounts counts twice, so per-account numbers add up to the total. Sent and engagement figures count actions. Core activity counts budget units.

Enrollment states are a snapshot of now, whatever the window: queued (waiting to start), active, paused, finished (any reason but a lost account, replied included), failed (the account became unavailable or was removed) and skipped. They are the same six as an Enrollment's status and a Motion's counts. progress groups them: waiting (queued), in_progress (active and paused), finished, and stopped (failed and skipped).

Core activity, per budget row

GET /v1/analytics/activity reports every budgeted LinkedIn call your accounts made, whoever triggered it: direct calls, Motion steps, scheduled posts and syncs share one budget. Per budget row, used is what the row's counter holds (successful and failed calls, as quotas counts them), failed is the failed part of it, and refused is the number of calls stopped before they reached LinkedIn. Each row counts its own unit, so no figure ever adds rows together:

Budget rowOne unit is
connection_requests_with_noteone invitation sent with a note
connection_requests_no_noteone invitation sent, with or without a note
messages_first_degreeone message to a 1st-degree connection
inmailone InMail sent
open_profile_inmailone free message to an Open Profile
profile_viewsone profile read
searchone search result line, not one query
commentsone comment posted
post_likesone reaction to a post, a comment or a message
followsone follow
content_pollone content read: a page of posts, a feed, a company page or its followers
job_pollone read of job postings
postsone post published
endorsementsone skill endorsed
invite_responsesone received invitation accepted or declined
withdrawal_sweepone sent invitation withdrawn
connections_syncone background read of the connection list
inbox_syncone background read of the inbox
profile_snapshotone attempt of the daily Account snapshot

Window and timezone

from and to are calendar days, YYYY-MM-DD, inclusive, read in timezone (IANA, default UTC). With neither, the window is the last 30 days. Core activity (GET /v1/analytics/activity) takes a custom window of at most 365 days. An event belongs to the day it happened in your timezone, so a late-evening action lands on the right day.

Lags

A step counts as soon as it settles. A reply counts within a minute of arriving. An acceptance counts once detected, about 2 hours after it happens. Read a fresh gap with that in mind.