Skip to content

Web Campaigns Reporting API ​

Programmatic access to web campaign analytics: visitors, sessions, signups, purchases, revenue, spend and ROAS, by campaign, channel, source or ad network hierarchy.

This API returns the numbers shown on the Web Campaigns page of the Linkrunner dashboard. For app campaigns (clicks, installs), use the Reporting API.

Your project needs Web Campaigns turned on. Otherwise every request returns 403.

Base URL ​

https://api.linkrunner.io/api/v1

Authentication ​

Pass your project's server key in the linkrunner-key header. This is the same key the Reporting API and other /api/v1 endpoints use. Find it under Settings → Data APIs:

linkrunner-key: YOUR_API_KEY

Postman 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 (429 with Retry-After: 60 when exceeded). This limit is separate from /reporting/campaigns, so you can pull app and web numbers in the same minute.
  • 30 req/sec per source IP (shared across all /api/v1 endpoints).
  • Cache responses for at least 60 seconds. Polling faster returns the same data.

Endpoint ​

GET https://api.linkrunner.io/api/v1/reporting/web-campaigns

Do not call the docs page URL (/api-reference/reporting-web-campaigns) as the API endpoint. The API path is /api/v1/reporting/web-campaigns.

Example request ​

bash
curl 'https://api.linkrunner.io/api/v1/reporting/web-campaigns?from=2026-10-01&to=2026-10-07&sort_field=revenue&limit=20' \
  -H 'linkrunner-key: YOUR_API_KEY'

Response ​

json
{
    "msg": "Web campaigns fetched successfully",
    "status": 200,
    "data": {
        "campaigns": [
            {
                "id": "summer_sale_fb",
                "name": "Summer Sale",
                "channel": null,
                "source": "facebook",
                "tracking_link": "https://example.com/?utm_id=summer_sale_fb&utm_source=facebook",
                "paused": false,
                "merged": false,
                "visitors": 3120,
                "sessions": 3980,
                "page_views": 10450,
                "signups": 241,
                "unique_signup_users": 228,
                "conversion": 7.31,
                "purchases": 58,
                "revenue": 48210.5,
                "spend": 12000,
                "roas": 4.02,
                "cost_per_visitor": 3.85,
                "custom_events": { "add_to_cart": { "count": 412 } }
            }
        ],
        "totals": { "visitors": 48210, "signups": 3120, "revenue": 612340, "...": "same metric fields as a row" },
        "pagination": { "total": 14, "pages": 1, "page": 1, "limit": 20, "truncated": false },
        "attribution": "last_touch",
        "group_by": "campaign",
        "network": null,
        "from": "2026-10-01",
        "to": "2026-10-07"
    }
}

Numbers are plain JSON numbers, not formatted strings. from and to in the response are the dates the numbers cover, including the defaults when you sent none.

pagination.total is the number of rows matching your filters, and pages is total / limit rounded up. totals covers every matching row, not only the current page. Unique counts such as visitors are deduplicated across the whole range, so totals.visitors is not the sum of the rows.

Query parameters ​

ParameterTypeDefaultNotes
from, tostringall timeYYYY-MM-DD, inclusive. start_date / end_date are accepted aliases. Without from, the range starts on the day your project was created. Without to, it ends today. An explicit range can span at most 366 days.
group_bystringcampaignWhat one row is. See Grouping.
networkstring(none)meta or google. Required with group_by=network_campaign, ad_set or ad, and refused with the others.
network_campaign_idstring(none)With the network groupings only: keep rows under this ad network campaign.
ad_set_idstring(none)With the network groupings only: keep rows under this ad set or ad group.
attributionstringlast_touchlast_touch or first_touch.
channelstring(all)A value returned in channel when group_by=channel, for example meta, google, paid_social, organic_search or direct.
sourcestring(all)A traffic source, for example facebook or google.
utm_source, utm_medium, utm_campaign, utm_id, utm_content, utm_termstring(all)Exact match on the UTM value.
searchstring(none)Partial match on the row id.
eventsstring(none)Comma-separated custom event names to count. See Custom events.
sort_fieldstringvisitorsvisitors, page_views, sessions, signups, unique_signup_users, purchases, revenue or name.
sort_orderstringdescendingascending or descending.
pagenumber11-indexed. Max 10000.
limitnumber10Max 100.

Send each parameter once. A repeated parameter, such as limit=5&limit=50, returns 400.

Grouping ​

group_byOne row isid is
campaignA web campaign (one utm_id)The utm_id. Without one, the utm_campaign value, which is where Google Ads puts its campaign ID.
channelA traffic channelThe channel key, for example meta or organic_search.
sourceA traffic sourceThe source, for example facebook.
network_campaignA Meta or Google Ads campaignThe ad network campaign ID.
ad_setA Meta ad set or Google Ads ad groupThe ad set or ad group ID.
adA Meta or Google Ads adThe ad ID.

network_campaign, ad_set and ad answer for one network at a time, so network is required with them. A channel or source from a different network, such as network=meta&source=google, returns 400.

Some rows collect traffic that has no key, such as visits without a utm_id. Their id is null and their name describes the bucket, for example (no campaign ID).

Row fields ​

FieldMeaning
id, nameSee Grouping. name is the campaign name when Linkrunner knows it, otherwise the same as id.
channel, sourceFilled where the grouping knows them. On group_by=campaign, channel is null and source is the campaign's traffic source. On the other groupings, source is null and the value is in id.
tracking_linkThe tracking link created in Linkrunner for this utm_id. null when there is none.
pausedtrue when the campaign is paused in Linkrunner. Paused campaigns stop counting from the time they were paused. null outside group_by=campaign.
mergedtrue when several utm_id values are reported as this one Meta campaign row. null outside group_by=campaign.
visitors, sessions, page_viewsUnique visitors, unique sessions, and page views.
signups, unique_signup_usersSignup events, and distinct signed-up users. Signups are counted from a fixed set of event names, such as signup and sign_up.
conversionVisitors who signed up, as a percentage of visitors.
purchases, revenuePurchase events, and the sum of the revenue sent with them. Purchases are counted from a fixed set of event names, such as purchase and order_completed.
spend, roas, cost_per_visitorAd spend matched from your connected ad accounts, revenue divided by spend, and spend divided by visitors. null where spend cannot be matched to the row, including every ad_set and ad row, because ad networks report spend per campaign.
custom_eventsCounts for the names you sent in events. {} when you sent none.

Revenue is summed as the amounts your website sent, without currency conversion. Spend keeps the currency of each ad account. Compare revenue, spend and roas only when your site and your ad accounts use one currency.

Days are counted in India Standard Time (Asia/Kolkata), whatever your project timezone. If your project uses another timezone, numbers for a single day can differ from your own day boundaries near midnight.

Custom events ​

Custom event columns are opt-in. Name the events you want in events, and each row carries their counts in custom_events:

bash
curl -G 'https://api.linkrunner.io/api/v1/reporting/web-campaigns' \
  -H 'linkrunner-key: YOUR_API_KEY' \
  --data-urlencode 'from=2026-10-01' \
  --data-urlencode 'to=2026-10-07' \
  --data-urlencode 'events=add_to_cart,checkout_started'
json
{
    "custom_events": {
        "add_to_cart": { "count": 412 },
        "checkout_started": { "count": 96 }
    }
}

count is the number of times the event happened. An event with no occurrences returns 0. You can name up to 50 events, each up to 120 characters. Longer lists return 422.

Ad network hierarchy ​

To list the ad sets of one Meta campaign, group by ad_set and pass the campaign ID from a group_by=network_campaign row:

bash
curl 'https://api.linkrunner.io/api/v1/reporting/web-campaigns?group_by=ad_set&network=meta&network_campaign_id=120200000000000001&from=2026-10-01&to=2026-10-07' \
  -H 'linkrunner-key: YOUR_API_KEY'

Use ad_set_id the same way with group_by=ad.

Errors ​

StatusWhen
400An unknown value for group_by, network, attribution, sort_field or sort_order; network missing with a network grouping, or sent with another grouping; a channel or source from a different network; a repeated parameter.
401Missing or invalid linkrunner-key.
402Billing account suspended. Body includes payment_link.
403Web Campaigns is not enabled for this project.
422limit is not between 1 and 100; page is not between 1 and 10000; a date is not YYYY-MM-DD; from is after to; the range is longer than 366 days; to is before your project was created; more than 50 events names or a name longer than 120 characters.
429Rate limit exceeded.
500Unexpected server error. Retry after a short backoff.

Troubleshooting ​

403 Web Campaigns not enabled for this project

A project admin turns on Web Campaigns in Settings → Project Details. See Track your website in an app project. Website projects have it on already.

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.

pagination.truncated is true

With group_by=campaign&channel=google, Linkrunner can list up to 1,000 Google Ads campaigns. When a project has more, truncated is true and the remaining campaigns are not in the response. Request a shorter date range.

Numbers don't match the dashboard

Compare the same date range, the same attribution model and the same grouping. Both the dashboard Web Campaigns page and this API default to one row per campaign.

TypeScript types ​

ts
export interface ReportingWebCampaignsResponse {
    msg: string;
    status: number;
    data: {
        campaigns: WebCampaignRow[];
        totals: WebCampaignMetrics;
        pagination: { total: number; pages: number; page: number; limit: number; truncated: boolean };
        attribution: "last_touch" | "first_touch";
        group_by: "campaign" | "channel" | "source" | "network_campaign" | "ad_set" | "ad";
        network: "meta" | "google" | null;
        from: string;
        to: string;
    };
}

export interface WebCampaignMetrics {
    visitors: number;
    sessions: number;
    page_views: number;
    signups: number;
    unique_signup_users: number | null;
    conversion: number;
    purchases: number;
    revenue: number;
    spend: number | null;
    roas: number | null;
    cost_per_visitor: number | null;
    custom_events: { [name: string]: { count: number } };
}

export interface WebCampaignRow extends WebCampaignMetrics {
    id: string | null;
    name: string;
    channel: string | null;
    source: string | null;
    tracking_link: string | null;
    paused: boolean | null;
    merged: boolean | null;
}

Need help? Contact support@linkrunner.io

Need help? Contact support@linkrunner.io