Appearance
Reporting API
Programmatic access to campaign analytics: clicks, installs, signups, spend, revenue, ROAS, ad sets, ad creatives, and keywords.
The Reporting API returns the same campaign analytics shown on the Linkrunner dashboard.
Base URL
https://api.linkrunner.io/api/v1Authentication
Pass your project's server key in the linkrunner-key header. This is the same key used by other /api/v1 endpoints. Find it under Settings → Data APIs:
linkrunner-key: YOUR_API_KEYPostman collection
Download the Postman collection. Import it into Postman, set the linkrunner_key collection variable to your key, and run any request.
Rate limit and freshness
- 1 request per minute per API key (
429withRetry-After: 60when exceeded). - 30 req/sec per source IP (shared across all
/api/v1endpoints). - Underlying analytics refresh on roughly the same cadence, so cache responses for at least 60 seconds.
Endpoint
GET https://api.linkrunner.io/api/v1/reporting/campaignsDo not call the docs page URL (/api-reference/reporting-campaigns) as the API endpoint. The API path is /api/v1/reporting/campaigns.
Example request
bash
curl 'https://api.linkrunner.io/api/v1/reporting/campaigns?from=2026-04-01&to=2026-04-30&network=meta&limit=20' \
-H 'linkrunner-key: YOUR_API_KEY'Response
json
{
"msg": "Campaigns fetched successfully",
"status": 200,
"data": {
"campaigns": [ /* see TypeScript types below */ ],
"pagination": { "total": 142, "pages": 8, "page": 1, "limit": 20 },
"display_currency": "USD",
"view": "user_acquisition"
}
}pagination.total is the number of campaigns matching your filters, and pages is total / limit rounded up.
Numeric fields are returned as formatted strings ("3,201", "$12,540.50"). Strip the formatting before doing math: Number(value.replace(/[^0-9.-]/g, "")).
Query parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
from, to | string | (none) | YYYY-MM-DD, inclusive, in your project's timezone. start_date / end_date are accepted aliases. Sets the metrics window; never filters the campaign list (see note below). |
active | string | (all) | true or false. |
network | string | (all) | meta, google, apple_search_ads, tiktok, snapchat, sandbox_ads, etc. |
meta_account_id | number | (none) | Restrict to a single Meta network account. |
platform | string | (both) | ios or android. |
search | string | (none) | Matches name, display ID, domain, deeplink, or full tracking URL. |
view | string | user_acquisition | user_acquisition (new users: installs, cost_per_install) or retargeting (re-engaged users: reinstalls, reengagements, cost_per_reinstall). |
sort_field | string | installs | clicks, installs, signups, revenue, spend, created_at, uninstalls, conversion, suspicious_installs, roas. |
sort_order | string | descending | ascending or descending. |
page | number | 1 | 1-indexed. |
limit | number | 10 | Max 100. Larger values return 422. |
from and to choose the window over which metrics (clicks, installs, revenue, spend) are computed. They do not filter which campaigns are returned: a campaign created after to still appears, with zeros for the window. This matches the dashboard campaign table, where the date picker changes the numbers, not the list. To fetch only campaigns created in a range, see Troubleshooting.
See Event columns below for events, payment_events, unique_user_events, cost_per_event, and active_users.
Event columns
Event columns are opt-in. Name the events you want and the response carries those and no others, which keeps the payload small and the query fast.
| Parameter | Effect | Example |
|---|---|---|
events | Return custom_events for these event names. | events=Purchase,AddToCart |
payment_events | Return payment_events for these revenue types. | payment_events=PURCHASE |
cost_per_event | Add cost_per_custom_events / cost_per_payment_events for these names (spend divided by that event's count). | cost_per_event=Purchase,PURCHASE |
unique_user_events | Make these events' count distinct users instead of total occurrences. The field name stays count. | unique_user_events=Purchase |
active_users | Override the active-users window, in days. Defaults to 7. | active_users=14 |
All five take a comma-separated list. Unknown event names are dropped silently, since they match no rows.
Naming an event in any of events, payment_events, cost_per_event or unique_user_events puts its count in the response, because a cost-per value is spend divided by that count and a distinct-user count replaces it. The extra columns are what you add on top:
events=Purchasereturnscustom_events.Purchaseand nothing else.cost_per_event=Purchasereturnscost_per_custom_events.Purchaseandcustom_events.Purchase.unique_user_events=Purchasereturnscustom_events.Purchasewhosecountis distinct users rather than occurrences.
Send none of them and the response contains no event columns at all: custom_events, payment_events, cost_per_custom_events and cost_per_payment_events all come back as {}. Every other metric (clicks, installs, signups, spend, revenue, ROAS, retention) is unaffected.
Find your project's event names under Dashboard → Events.
This changed on 2026-09-11. Previously the endpoint tried to infer which events you wanted from the saved dashboard column settings of one of your project admins, so the same request returned different events for different projects, changed whenever that person edited their dashboard, and gave no way to tell which events you were looking at. Those settings are no longer read.
If you were relying on events appearing without asking for them, add an events= list naming them. If a count dropped, that event was previously being counted per distinct user: add it to unique_user_events as well.
Example: two events, with cost-per and distinct users
bash
curl -G 'https://api.linkrunner.io/api/v1/reporting/campaigns' \
-H 'linkrunner-key: YOUR_API_KEY' \
--data-urlencode 'from=2026-04-01' \
--data-urlencode 'to=2026-04-30' \
--data-urlencode 'events=Purchase,AddToCart' \
--data-urlencode 'cost_per_event=Purchase' \
--data-urlencode 'unique_user_events=Purchase'Each campaign, ad set and ad creative in the response then carries:
json
{
"custom_events": {
"Purchase": { "count": "412", "amount": "0.00" },
"AddToCart": { "count": "1,905", "amount": "0.00" }
},
"cost_per_custom_events": { "Purchase": "258.20" },
"payment_events": {},
"cost_per_payment_events": {}
}Purchase reports distinct users because it was named in unique_user_events; AddToCart reports total occurrences. Both use the count field, so the parameter you sent is the only thing that tells them apart. payment_events is empty because no revenue types were requested.
Errors
| Status | When |
|---|---|
401 | Missing or invalid linkrunner-key. |
402 | Billing account suspended. Body includes payment_link. |
422 | limit is non-numeric, less than 1, or greater than 100. |
429 | Rate limit exceeded. |
500 | Unexpected server error. Retry after a short backoff. |
Troubleshooting
Campaigns created outside my from/to range are in the response
This is expected. from and to set the metrics window; they never remove campaigns from the list. To keep only campaigns created in a range, request sort_field=created_at&sort_order=descending, then drop rows client-side once created_at falls before from.
429 Too Many Requests on the second call
The limit is 1 request per minute per API key. Honor the Retry-After: 60 header and cache responses for at least 60 seconds. The underlying analytics refresh on roughly the same cadence, so polling faster returns the same data.
Numbers don't match the dashboard
Compare the same date range and the same view (user_acquisition vs retargeting). The API interprets from and to in your project's timezone, exactly like the dashboard date picker. If you are doing math on the values, remember they are formatted strings (see the warning under Response).
TypeScript types
ts
export interface ReportingCampaignsResponse {
msg: string;
status: number;
data: {
campaigns: Campaign[];
pagination: { total: number; pages: number; page: number; limit: number };
display_currency?: string;
view?: "user_acquisition" | "retargeting";
};
}
export interface Campaign {
id: number;
active: boolean;
created_at: string;
display_id: string;
link: string;
name: string;
// Counts (formatted strings)
clicks: string;
installs: string;
reinstalls?: string;
reengagements?: string;
"sign-ups": string;
uninstalls?: string;
suspicious_installs?: string;
click_through_attribution?: string;
view_through_attribution?: string;
conversion: string;
// Money (formatted strings, in display_currency)
revenue: string;
revenue_event_count?: string;
spend: string;
roas?: string;
cost_per_install?: string;
cost_per_reinstall?: string;
cost_per_signup?: string;
// Network flags
meta: boolean;
meta_web_to_app: boolean;
google: boolean;
google_web_to_app: boolean;
tiktok?: boolean;
snapchat?: boolean;
linkedin?: boolean;
ad_network_code?: string;
// Platforms
ios: boolean;
android: boolean;
// Misc
activity_in_last_x_days: number;
has_date_filter: boolean;
domain?: { id: number; name: string } | null;
custom_store_listing: string | null;
active_users?: number | string;
// Custom & payment events: map of event name → count/amount
custom_events?: { [name: string]: { count: string; amount?: string } | string };
payment_events?: { [type: string]: { count: string; total: string } };
cost_per_custom_events?: { [name: string]: string };
cost_per_payment_events?: { [type: string]: string };
// Retention buckets (rates as percentages). `retention` carries cumulative retention.
retention: { d1: number | string; d7: number | string; d14: number | string; d30: number | string };
rolling_retention?: { d1: number | string; d7: number | string };
classic_retention?: { d1?: number | string; d7?: number | string; d14?: number | string; d30?: number | string };
daywise_revenue?: { d0?: number | string; d3?: number | string; d7?: number | string; d30?: number | string };
// Connected ad-account (Meta / Google / Apple Search Ads)
network_account?: {
id: number;
name: string;
email: string;
status: string;
capi_configured: boolean;
account_name?: string;
external_customer_id?: string;
} | null;
// Hierarchy: campaign → adSets → adCreatives + keywords
adSets?: AdSet[];
}
export interface AdSet {
id: string;
name: string;
clicks: string;
installs: string;
"sign-ups": string;
spend: string;
revenue: string;
revenue_event_count?: string;
conversion: string;
roas?: string;
cost_per_install?: string;
cost_per_signup?: string;
suspicious_installs?: string;
custom_events?: Campaign["custom_events"];
payment_events?: Campaign["payment_events"];
cost_per_custom_events?: { [name: string]: string };
cost_per_payment_events?: { [type: string]: string };
daywise_revenue?: Campaign["daywise_revenue"];
ios: boolean;
android: boolean;
retention: { d1: number | string; d7: number | string; d14: number | string; d30: number | string };
rolling_retention?: { d1: number | string; d7: number | string };
adCreatives?: AdCreative[];
keywords?: Keyword[];
}
export interface AdCreative {
id: string;
name: string;
type: string;
deeplink: number;
clicks: string;
installs: string;
"sign-ups": string;
spend: string;
revenue: string;
revenue_event_count?: string;
conversion: string;
roas?: string;
cost_per_install?: string;
cost_per_signup?: string;
custom_events?: Campaign["custom_events"];
payment_events?: Campaign["payment_events"];
cost_per_custom_events?: { [name: string]: string };
cost_per_payment_events?: { [type: string]: string };
retention: { d1: number | string; d7: number | string; d14: number | string; d30: number | string };
rolling_retention?: { d1: number | string; d7: number | string };
}
// Apple Search Ads keyword-level rows. Same shape as AdCreative metrics.
export interface Keyword {
id: string;
name: string;
clicks: string;
installs: string;
"sign-ups": string;
spend: string;
revenue: string;
revenue_event_count?: string;
conversion: string;
roas?: string;
cost_per_install?: string;
cost_per_signup?: string;
suspicious_installs?: string;
custom_events?: Campaign["custom_events"];
payment_events?: Campaign["payment_events"];
cost_per_custom_events?: { [name: string]: string };
cost_per_payment_events?: { [type: string]: string };
retention?: { d1: number | string; d7: number | string; d14: number | string; d30: number | string };
rolling_retention?: { d1: number | string; d7: number | string };
}Need help? Contact support@linkrunner.io