Goals and acquisition
Define conversion goals, correct identities, and read acquisition reports built on confirmed sends.
These endpoints turn product events into conversions and report them against the campaign sends that preceded them. The concepts are explained in the acquisition guide.
Reads need READ_ANALYTICS (view_analytics). Prospect lists, timelines, acquired-customer lists, and attribution explanations are contact data, so they also need READ_CONTACTS (view_contacts). Anything that changes goals, identities, spend, or attribution needs MANAGE_ACQUISITION (manage_settings).
Goals
A goal matches one event name, optionally filtered by properties of its data, and optionally reads a monetary value from it. Its kind is a funnel stage: signup, activation, paid, revenue, retention, or custom. Each stage except custom has at most one primary goal, and headline numbers read only primary goals, so two goals that match the same events never double a total.
The goal definition
| Field | Type | Description |
|---|---|---|
event_name | string | Event to match. |
predicates | array | Filters, all of which must hold. Up to 20. |
value_path | string | Dot path to a numeric amount in data, such as amount or invoice.total. Optional. |
value_unit | string | minor (cents, the default) or major (dollars). Major amounts are converted with the currency's decimal places. |
currency_path | string | Dot path to an ISO 4217 code in data. |
default_currency | string | Code used when the event carries none. A value needs one of the two. |
A predicate is { "path", "op", "value" }. path is a dot path of up to 8 segments. op is one of:
op | value | Matches when the property |
|---|---|---|
eq, neq | string, number, or boolean | equals, or does not equal (a missing property does not equal anything) |
in, not_in | array of 1 to 50 scalars | is, or is not, one of them |
gt, gte, lt, lte | number | is a number compared that way |
contains | string | is a string containing it, ignoring case |
exists, not_exists | none | is present, or absent |
Numbers compare by value, so 1 equals 1.0, but the string "1" never equals the number 1. A value that is missing, not a positive whole number of minor units, or has no valid currency leaves the conversion without a value; the conversion still counts.
Revisions and replay
Editing a goal's definition creates a new revision that applies to events received from then on. Events received earlier keep the conversions their revision gave them. Renaming a goal or changing which goal is primary does not create a revision. To apply the current revision to the past, preview and then run a replay over a range of up to 366 days. Conversions whose raw event has already been pruned cannot be re-evaluated and are kept.
| Method | Path | Scope |
|---|---|---|
GET | /goals | READ_ANALYTICS. ?include_archived=true adds archived goals. |
POST | /goals | MANAGE_ACQUISITION. Body: name, kind, is_primary, definition. Making a goal primary demotes the stage's current primary. |
POST | /goals/preview | READ_ANALYTICS. Body: definition, days (default 30). Writes nothing. |
GET | /goals/:id | READ_ANALYTICS. Includes every revision. |
PATCH | /goals/:id | MANAGE_ACQUISITION. Any of name, is_primary, definition. The stage cannot change. |
DELETE | /goals/:id | MANAGE_ACQUISITION. Archives: the goal stops matching and keeps its conversions. |
POST | /goals/:id/replays/preview | READ_ANALYTICS. Body: from, to. Writes nothing. |
POST | /goals/:id/replays | MANAGE_ACQUISITION. Body: from, to. Answers 202 with the replay. |
GET | /goals/:id/replays | READ_ANALYTICS. The 20 most recent. |
GET | /goals/:id/replays/:replayId | READ_ANALYTICS. |
{
"name": "Pro signup",
"kind": "signup",
"is_primary": true,
"definition": {
"event_name": "account.created",
"predicates": [{ "path": "plan", "op": "in", "value": ["pro", "team"] }]
}
}A replay preview answers scanned, would_add, would_remove, would_keep, and retained_without_event. A replay is queued, running, completed, or failed; one that finds the goal edited or archived underneath it stops with error: "goal_changed". Only one replay per goal runs at a time.
People and identity conflicts
Every event belongs to a person. A person may have a contact, and may be known by any number of emails, user ids, and anonymous ids. People without a contact are listed so you can see who is using the product but is not a prospect yet.
| Method | Path | Scope |
|---|---|---|
GET | /people | READ_CONTACTS. People without a contact, newest first, with their identifiers, accounts, and event counts. |
GET | /people/:id | READ_CONTACTS. |
DELETE | /people/:id | MANAGE_ACQUISITION. Erases a person without a contact, with their events and conversions. A person with a contact answers 409 person_has_contact: delete the contact instead. |
GET | /identity-conflicts | READ_CONTACTS. ?status=open (default), dismissed, or all. |
POST | /identity-conflicts/:id/resolve | MANAGE_ACQUISITION. Body: { "action": "merge" } or { "action": "dismiss" }. |
Identity conflicts
A conflict records that an identifier claimed by one person already belongs to another. person_id is who the event or link resolved to; other_person_id already held the identifier. merge folds the other person into person_id with their identifiers, events, accounts, and conversions. Two people with different contacts cannot be merged (409 identity_merge_contacts). dismiss keeps them apart.
Deleting a contact erases their person, events, and conversions with it, the same way it erases their website visits.
Reports
Reports follow confirmed sends: sends a worker confirmed, never reservations or failures. A send is followed for window_days (default 30, up to 365). Each stage counts distinct prospects who reached it after a send and within its window: replied and positive_reply from campaign reply classification, signup, activation, and paid from each stage's primary goal.
per_10k_sends is mature_people / mature_sends × 10,000, where mature means the send's window has closed. Newer sends are counted but kept out of the rate, so a fresh cohort never looks worse than it is. These are send-cohort numbers, not attribution: someone emailed by two campaigns counts in both.
| Method | Path | Scope |
|---|---|---|
GET | /acquisition/summary | READ_ANALYTICS |
GET | /acquisition/breakdown | READ_ANALYTICS |
POST | /acquisition/event-trends | READ_ANALYTICS. Writes nothing. |
POST | /acquisition/funnels | READ_ANALYTICS. Writes nothing. |
GET | /acquisition/prospects | READ_ANALYTICS and READ_CONTACTS |
GET | /acquisition/prospects/:contactId/timeline | READ_ANALYTICS and READ_CONTACTS |
PUT | /acquisition/campaign-spend/:campaignId | MANAGE_ACQUISITION. Body: amount_minor, currency. |
DELETE | /acquisition/campaign-spend/:campaignId | MANAGE_ACQUISITION |
Summary and breakdown take from and to (RFC 3339, default the last 30 days, at most 366 days apart), window_days, and campaign_id.
Summary
send_cohort is the whole range as one breakdown row. calendar_period is separate: first_conversions counts people whose first conversion for each primary goal happened inside the range, and revenue_received sums the primary revenue goal's values that arrived in the range, whenever those people were emailed. primary_goals shows which stages are configured; a stage without one reports nothing. unmatched_people and open_identity_conflicts show identity health.
Breakdown
dimension is one of campaign, step, variant, offer, angle, cta, mailbox, audience_source, segment, category, or all. Variant, offer, angle, and CTA come from the frozen send snapshot; sends from before snapshots existed report (unknown) rather than a guess. Audience dimensions use the audience recorded when the contact entered the campaign. A send appears once per category the contact had, so category rows overlap.
Each row has confirmed_sends, mature_sends, prospects, stages, and window_revenue (per currency, never summed across currencies). Campaign rows for campaigns with spend carry spend: the amount, lifetime signups and paid customers within the window after any of the campaign's sends, and cost_per_signup_minor and cost_per_paid_minor. Cost is never shown without explicit spend.
Trends and funnels
POST /acquisition/event-trends takes name, optional predicates, from, to, and interval (day or week), and returns occurrences and unique people per bucket.
POST /acquisition/funnels takes 2 to 8 ordered steps, from, to, and window_days. A step is { "kind": "event", "event_name", "predicates" }, { "kind": "goal", "goal_id" }, { "kind": "confirmed_send", "campaign_id" }, or { "kind": "positive_reply", "campaign_id" }. People enter at their first step-one row in the range; each later step must happen after the previous one and within window_days of entering. The response gives each step's people, conversion from the previous and first step, drop-off, and largest_drop_off_step.
Prospects
GET /acquisition/prospects lists contacts enrolled in any campaign (or campaign_id) with their first and last confirmed send, first reply and positive reply, first signup, activation, and paid conversion, lifetime revenue per currency, and stage. Filter with filter (interested, signed_up, activated, paid, silent for conversions without a reply) or dropoff (sent_no_reply, interested_no_signup, signup_no_activation, activation_no_paid, signup_no_paid), and search with q. Pages are newest activity first, with limit up to 200 and an opaque cursor.
GET /acquisition/prospects/:contactId/timeline merges enrollments, confirmed sends (with step, variant, mailbox, offer, angle, CTA), replies, Reply Agent drafts, product events, and conversions, newest first. Each conversion carries last_send_before, the last confirmed send at or before it. That is context, not an attribution decision.
Customer attribution
Attribution credits the last confirmed send at or before a person's first primary signup conversion, or first primary paid conversion when no signup goal is primary, within the workspace window. The default is 30 days. The persisted acquisition survives later campaigns and recurring revenue inherits it. Occurrence time decides late events; unknown historical dimensions stay unknown. Company estimates use colleagues in a customer account or corporate domain and are reported separately from exact credit.
| Method | Path | Scope |
|---|---|---|
GET | /acquisition/attribution/settings | READ_ANALYTICS |
PUT | /acquisition/attribution/settings | MANAGE_ACQUISITION |
GET | /acquisition/attribution/summary, /acquisition/attribution/breakdown | READ_ANALYTICS |
GET | /acquisition/attributions, /acquisition/attributions/:personId | READ_ANALYTICS and READ_CONTACTS |
GET | /acquisition/prospects/:contactId/attribution | READ_ANALYTICS and READ_CONTACTS |
PUT, DELETE | /acquisition/attributions/:personId/correction | MANAGE_ACQUISITION |
Policy
Settings are window_days (1 to 365) and company_estimates (default true), with updated_by and updated_at. PUT replaces supplied fields and preserves omitted fields. A policy change re-evaluates automatic unattributed acquisitions; already credited customers keep their credit. Turning estimates off removes estimated credits, and turning them on queues recalculation.
Attribution reports
Summary and breakdown accept from, to, and optional campaign_id, using the same range bounds as cohort reports. These ranges select acquisition dates, not send dates. window_days on these requests does not override workspace policy.
Summary returns settings, acquisition_goal, customers, unattributed, attributed, attributed_without_engagement, match_methods, corrected, company_estimate, and pending. The attributed and company_estimate blocks each contain customers, paid, and lifetime_revenue. Estimates are never added to exact totals. Revenue uses the current primary revenue goal, keeps currencies separate, and includes conversions outside the acquisition date range. No revenue goal means an empty revenue array. The without-engagement count means no recorded reply, open, or click at acquisition; it does not require engagement for credit.
Breakdown accepts the cohort dimensions except all, plus match_method. It returns data rows with key, label, exact customers, paid, without_engagement, lifetime_revenue, and a separate company_estimate. At most 100 groups are returned; category groups overlap.
Acquired customers and explanations
The acquired-customer list accepts the range, optional campaign_id, status (attributed, unattributed, or estimated), limit (up to 200), and opaque cursor. It returns data and pagination, newest acquisition first. Each row includes the person and optional contact, acquisition date, match method, source, exact credit, optional company estimate, and lifetime revenue. Unattributed includes people with a company estimate.
The person and contact explanation routes return acquisition, credit, company_estimate, lifetime_revenue, pending, up to 100 recent touches, up to 50 recent corrections, and up to 20 recent candidate_sends. The contact route returns an empty explanation when no person exists yet. Each touch records its own last confirmed send and window without moving the acquisition. Match methods are contact, email, linked_identity, website_visitor, unknown, or none for no exact credit. Credits expose observed reply, open, click, and visit times separately, plus frozen campaign, step, variant, mailbox, copy, and audience dimensions. send_available: false means the original send was deleted but the credit snapshot survives.
Corrections
PUT correction takes { "send_id": "confirmed-send-uuid", "reason": "Why this send won" }, or send_id: null to mark the person as not acquired by outbound. A non-empty reason of at most 500 characters is required. The send must belong to the same workspace and contact and be confirmed at or before acquisition. A correction may select a send outside the automatic window. It records actor and history, persists across new evidence, and changes downstream revenue totals. DELETE correction restores automatic attribution from current evidence, optionally with a reason query parameter.
Repeating the same settings write, correction, or restore has no additional effect, so these replacement operations are naturally safe to retry without an Idempotency-Key. Other acquisition writes retain their normal idempotency middleware.