VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Essentials
API overview
Authentication
Event capture
Push device API
Webhooks
Webhook delivery
Webhook events
Projects & keys
Projects
API keys
Audit log
Customers & access
Persons
Products
Perks
Sandbox
Paywalls
Paywalls
Paywall locations
Components
Component registry
Assets
Experiments & flags
Experiments
Feature flags
Analytics & events
Analytics
Analytics alerts
Events
Ingest policy
VoidQL
Tracking domains
Ads
Ads
Ad accounts
Meta measurement
Payments & stores
Payment providers
Payment provider products
Purchase history imports
Payment sources
Webhooks & notifications
Webhooks
Push notifications
Push configurations
Automations
Organization & team
Organizations
Members
Invitations
Billing
Your account
Profile
Session
Notices
Connected apps
Terms of Service
Developer tools
Schema
SDK
Compute insights
Compute charts
Report acquisition
Report acquisition cohorts
Report tracking health
List live users

Report acquisition

Groups the people a network, campaign, ad set or ad brought in, or people by the surface they signed up on (web, iOS or Android), and reports what Voidhash measured for them on the web and in apps: visits, signups, measured app installs, trials, payers, revenue and refunds in one currency, the funnel by surface, where payers converted and the project's revenue coverage by credited touch.

A signup is credited to the person's last touch with an ad network before it within the attribution window (the project's, unless attributionWindowDays overrides it), else to their last touch of any kind in that time, or to a touch up to an hour after it; a signup without one is unattributed. The person is acquired on that touch's day and their later revenue stays with it. People count from the earliest of signing up and first paying or starting a trial, so someone who buys and creates an account later stays with the touch before the purchase; other anonymous visitors count only as visits. Revenue counts once on the person an identify merge keeps, wherever they pay, and only production revenue counts by default. Money is in integer minor units of currency, such as cents. People a touch reached only after they had signed up, such as customers reached by retargeting, are reported only as returningPeople, returningPayers and returningRevenue, which are never part of other revenue. selfReported lists what at least 2 unattributed people answered in the $acquisition_source person trait.

Guide: Analytics overview

POST
/api/v1/projects/{projectId}/analytics/reports/acquisition

Authorization

AuthorizationBearer <token>

A project secret key: vh_sk_... for production, vh_sk_test_... for test. Keep it on your servers.

In: header

Path parameters

projectId*string

The project's id, proj_.... A secret key works only in the project it belongs to.

Request body

application/json

TypeScript definitions

Use the request body type in TypeScript.

adId?|

Only people whose credited touch carried this ad id.

adsetId?|

Only people whose credited touch carried this ad set id.

attributionWindowDays?|

How many days, 1 to 90, before a signup a touch can be credited with it. Defaults to the project's attribution window.

campaignId?|

Only people whose credited touch carried this campaign id.

currency?|

The ISO 4217 code money is reported in. Amounts in another currency are converted at each day's exchange rate. Defaults to USD.

environment?|

The environment whose revenue counts: production (the default), sandbox or test. Visits come from production keys, or from test keys for test; signups count in every environment.

from*string

The first acquisition day, YYYY-MM-DD, in the report time zone.

hasDailyRows?|

Whether every row also carries days, its outcomes per acquisition day.

hasSurfaceBreakdown?|

Whether every row also carries signupSurfaces and paymentSurfaces, its outcomes by signup and payment surface.

keyConversion?|

The conversion ad cost is measured against: signup, trial or payer. Omitted or null uses the project's setting.

level*string

What rows group by: the credited touch's network, campaign, adset or ad, or surface, where people group by the surface they signed up on, or cross_surface.

Value in

  • "network"
  • "campaign"
  • "adset"
  • "ad"
  • "surface"
network?|

Only people whose credited touch came from this ad network, such as meta or google. Compared in lowercase.

revenueWindow?||

Which revenue, signups and installs count: the first days after each person's acquisition, or everything up to the end of the until day. Defaults to everything up to now, so recent people have had less time to pay.

timezone?|

The IANA time zone that sets day boundaries, such as the ad account's. Offsets such as +01:00 are rejected. Defaults to UTC.

to*string

The last acquisition day, inclusive, YYYY-MM-DD. The range spans at most 366 days.

Examples

curl -X POST "https://api.voidhash.com/api/v1/projects/proj_tz4a98xxat96iws9zmbrgj3a/analytics/reports/acquisition" \  -H "Authorization: Bearer vh_sk_..." \  -H "Content-Type: application/json" \  -d '{    "from": "2026-03-01",    "to": "2026-03-31",    "level": "network"  }'
{  "projectId": "proj_tz4a98xxat96iws9zmbrgj3a",  "level": "network",  "network": null,  "campaignId": null,  "adsetId": null,  "adId": null,  "currency": "USD",  "timezone": "UTC",  "environment": "production",  "keyConversion": "payer",  "from": "2026-03-01",  "to": "2026-03-31",  "start": "2026-03-01T00:00:00.000Z",  "end": "2026-04-01T00:00:00.000Z",  "revenueWindow": {    "kind": "all"  },  "attributionWindowDays": 7,  "revenueEnd": "2026-04-02T08:00:00.000Z",  "groups": [    {      "peopleAcquired": 1046,      "signups": 1046,      "visits": 7020,      "appInstalls": 1204,      "trialStarters": 412,      "trialConversions": 131,      "payingPeople": 168,      "crossSurfacePayers": 97,      "keyConversions": 168,      "signupToPayerRate": 0.1606,      "returningPeople": 23,      "returningPayers": 7,      "returningRevenue": 6993,      "grossRevenue": 268430,      "refunds": 4995,      "netRevenue": 263435,      "network": "meta",      "campaignId": null,      "campaignName": null,      "adsetId": null,      "adsetName": null,      "adId": null,      "adName": null,      "surface": null    },    {      "peopleAcquired": 214,      "signups": 214,      "visits": 1390,      "appInstalls": 120,      "trialStarters": 71,      "trialConversions": 24,      "payingPeople": 38,      "crossSurfacePayers": 12,      "keyConversions": 38,      "signupToPayerRate": 0.1776,      "returningPeople": 0,      "returningPayers": 0,      "returningRevenue": 0,      "grossRevenue": 52962,      "refunds": 0,      "netRevenue": 52962,      "network": "newsletter",      "campaignId": null,      "campaignName": null,      "adsetId": null,      "adsetName": null,      "adId": null,      "adName": null,      "surface": null    }  ],  "unattributed": {    "peopleAcquired": 1880,    "signups": 1880,    "visits": 0,    "appInstalls": 1702,    "trialStarters": 604,    "trialConversions": 198,    "payingPeople": 251,    "crossSurfacePayers": 4,    "keyConversions": 251,    "signupToPayerRate": 0.1335,    "returningPeople": 0,    "returningPayers": 0,    "returningRevenue": 0,    "grossRevenue": 391488,    "refunds": 9990,    "netRevenue": 381498  },  "totals": {    "peopleAcquired": 3140,    "signups": 3140,    "visits": 8410,    "appInstalls": 3026,    "trialStarters": 1087,    "trialConversions": 353,    "payingPeople": 457,    "crossSurfacePayers": 113,    "keyConversions": 457,    "signupToPayerRate": 0.1455,    "returningPeople": 23,    "returningPayers": 7,    "returningRevenue": 6993,    "grossRevenue": 712880,    "refunds": 14985,    "netRevenue": 697895  },  "selfReported": [    {      "answer": "podcast",      "people": 212,      "netRevenue": 48951    },    {      "answer": "friend",      "people": 167,      "netRevenue": 37962    },    {      "answer": "app store search",      "people": 94,      "netRevenue": 19980    }  ],  "funnel": [    {      "step": "visits",      "count": 8410,      "surfaces": [        {          "surface": "web",          "count": 8410        }      ]    },    {      "step": "app_installs",      "count": 1324,      "surfaces": [        {          "surface": "ios",          "count": 1012        },        {          "surface": "android",          "count": 312        }      ]    },    {      "step": "signups",      "count": 1260,      "surfaces": [        {          "surface": "web",          "count": 655        },        {          "surface": "ios",          "count": 470        },        {          "surface": "android",          "count": 135        }      ]    },    {      "step": "trials",      "count": 483,      "surfaces": [        {          "surface": "ios",          "count": 371        },        {          "surface": "android",          "count": 112        }      ]    },    {      "step": "payers",      "count": 206,      "surfaces": [        {          "surface": "web",          "count": 89        },        {          "surface": "ios",          "count": 98        },        {          "surface": "android",          "count": 19        }      ]    }  ],  "convertedOn": {    "surfaces": [      {        "surface": "web",        "payingPeople": 112,        "grossRevenue": 172540,        "refunds": 4995,        "netRevenue": 167545      },      {        "surface": "ios",        "payingPeople": 289,        "grossRevenue": 456120,        "refunds": 9990,        "netRevenue": 446130      },      {        "surface": "android",        "payingPeople": 56,        "grossRevenue": 84220,        "refunds": 0,        "netRevenue": 84220      }    ],    "crossSurfacePayers": 113,    "journeys": [      {        "touch": "web",        "signup": "web",        "payment": "ios",        "payingPeople": 71      },      {        "touch": "web",        "signup": "ios",        "payment": "ios",        "payingPeople": 30      },      {        "touch": "web",        "signup": "web",        "payment": "android",        "payingPeople": 8      },      {        "touch": null,        "signup": "web",        "payment": "ios",        "payingPeople": 4      }    ]  },  "coverage": {    "attributedRevenue": 528940,    "totalRevenue": 1486220,    "share": 0.3559,    "netRevenue": 1458310,    "classes": [      {        "class": "paid",        "network": "meta",        "reason": null,        "netRevenue": 401250,        "share": 0.2751      },      {        "class": "organic",        "network": null,        "reason": null,        "netRevenue": 112640,        "share": 0.0772      },      {        "class": "none",        "network": null,        "reason": "no_touch",        "netRevenue": 902380,        "share": 0.6188      },      {        "class": "none",        "network": null,        "reason": "outside_window",        "netRevenue": 30120,        "share": 0.0207      },      {        "class": "none",        "network": null,        "reason": "no_person",        "netRevenue": 11920,        "share": 0.0082      }    ]  }}

{  "error": {    "type": "invalid_request",    "code": "invalid_report_request",    "message": "At most 20 insight queries are allowed per batch"  }}

{  "error": {    "type": "unauthenticated",    "code": "invalid_api_key",    "message": "This secret key is not valid."  }}
{  "error": {    "type": "permission_denied",    "code": "permission_denied",    "message": "Insufficient permissions."  }}
{  "error": {    "type": "not_found",    "code": "project_not_found",    "message": "There is no project proj_tz4a98xxat96iws9zmbrgj3a, or this credential cannot use it.",    "param": "projectId"  }}
{  "error": {    "type": "internal",    "code": "internal_error",    "message": "Something went wrong on our side. Try again, and contact support with the Request-Id header if it keeps failing."  }}

Response body

application/json

application/json

application/json

application/json

application/json

application/json

Errors

StatustypecodeWhen
400invalid_requestinvalid_report_requestFor a request over the size limits or in a currency without exchange rates for the period, with a message saying what to change.
400invalid_requestinvalid_time_rangeFor a bad range.

It can also fail with the errors every operation shares, such as unauthenticated; see Errors.

More in Analytics

  • Compute insights POST
  • Compute charts POST
  • Report acquisition cohorts POST
  • Report tracking health POST
  • List live users POST

Something wrong or missing on this page? Tell us by email or on Discord.

PreviousCompute chartsNextReport acquisition cohorts

On this page

AuthorizationPath parametersRequest bodyExamplesResponse bodyErrorsMore in Analytics