> ## Documentation Index
> Fetch the complete documentation index at: https://docs.linkrunner.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 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/v1
```

## Authentication

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](https://dashboard.linkrunner.io/settings?s=data-apis):

```
linkrunner-key: YOUR_API_KEY
```

## Postman collection

[Download the Postman collection](https://gist.githubusercontent.com/RathodDarshil/20d04e80c2d97f8280e1b3beffb58465/raw/reporting-campaigns.postman_collection.json). 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).
* 30 req/sec per source IP (shared across all `/api/v1` endpoints).
* 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/campaigns
```

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

### Example request

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    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'
    ```
  </Tab>

  <Tab title="Google Apps Script">
    ```js theme={null}
    const response = UrlFetchApp.fetch(
        "https://api.linkrunner.io/api/v1/reporting/campaigns?from=2026-04-01&to=2026-04-30&limit=20",
        {
            method: "get",
            headers: {
                "linkrunner-key": "YOUR_API_KEY",
            },
        }
    );

    const data = JSON.parse(response.getContentText());
    ```
  </Tab>
</Tabs>

### Response

```json theme={null}
{
    "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.

<Warning>
  Numeric fields are returned as **formatted strings** (`"3,201"`, `"$12,540.50"`). Strip the formatting before doing math: `Number(value.replace(/[^0-9.-]/g, ""))`.
</Warning>

### 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`.                                                                                                                                        |

<Note>
  `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](#troubleshooting).
</Note>

See [Event columns](#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=Purchase` returns `custom_events.Purchase` and nothing else.
* `cost_per_event=Purchase` returns `cost_per_custom_events.Purchase` **and** `custom_events.Purchase`.
* `unique_user_events=Purchase` returns `custom_events.Purchase` whose `count` is 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](https://dashboard.linkrunner.io/dashboard/events).

<Note>
  **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.
</Note>

#### Example: two events, with cost-per and distinct users

```bash theme={null}
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 theme={null}
{
    "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

<AccordionGroup>
  <Accordion title="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`.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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](#response)).
  </Accordion>
</AccordionGroup>

## TypeScript types

```ts theme={null}
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](mailto:support@linkrunner.io)
