Pull your clicks, conversions and aggregated statistics from Swaarm programmatically through the GraphQL API.
This guide covers three reports:
You want… | GraphQL query | Where it lives in the Swaarm UI |
Every individual click, one row per click |
| Click Report |
Every individual conversion (postback), one row each |
| Conversion Report |
Aggregated numbers grouped by any dimensions you pick |
| Custom Report |
1. Before you start
You need:
Your API endpoint. Every Swaarm platform has its own:
https://api.<your-platform>.swaarm-clients.com/graphql
<your-platform>is the same name you see in your Swaarm dashboard address. If you are unsure, ask your account manager.A Swaarm user account with access to reports. We recommend creating a dedicated user for API access (for example
api@yourcompany.com) so integrations keep working when team members change.A GraphQL or HTTP client. Any language works. The examples below use
curlandjq, plus a Python script in section 9.
Tip — explore interactively. The full schema can be explored with any GraphQL IDE (Altair, Insomnia, Postman). Point it at your endpoint, add the Authorization header from section 2, and use introspection to browse every field and enum value mentioned in this guide.
2. Authentication
The API uses bearer tokens. You log in once, receive an access token, and send it with every request.
2.1 Log in
mutation Login($input: AuthLoginInput!) { login(input: $input) { authChallenge token { accessToken refreshToken expiresIn } } }Variables:
{ "input": { "username": "api@yourcompany.com", "password": "your-password" } }With curl:
API=https://api.<your-platform>.swaarm-clients.com/graphql curl -s "$API" \ -H 'Content-Type: application/json' \ -d '{ "query": "mutation($input: AuthLoginInput!){ login(input:$input){ authChallenge token { accessToken refreshToken expiresIn } } }", "variables": { "input": { "username": "api@yourcompany.com", "password": "your-password" } } }'What you get back:
Field | Meaning |
| Send this on every request (see 2.2). |
| Use it to get a new access token without sending the password again. |
| Access-token lifetime in seconds. |
|
|
2.2 Send the token
Add this header to every request:
Authorization: Bearer <accessToken>
2.3 Refresh the token
When the access token expires, requests are rejected. Get a new one with the refresh token:
mutation Refresh($input: AuthRefreshTokenInput!) { refreshToken(input: $input) { token { accessToken refreshToken expiresIn } } }Variables: { "input": { "refreshToken": "<refreshToken>" } }
Always store the new refreshToken from the response; the old one may stop working.
3. Making requests
All reports go through the same endpoint with a POST:
POST https://api.<your-platform>.swaarm-clients.com/graphql Content-Type: application/json Authorization: Bearer <accessToken> { "query": "...", "variables": { ... } }The response is always JSON:
{ "data": { ... }, "errors": [ ... ] }Check for an errors array even when the HTTP status is 200.
Two report styles
The three reports return data in two different shapes. Knowing this up front makes the rest of the guide easier.
Row-level reports (Click Report, Conversion Report) return one row per event. Each row is a list of records, and each record is a field + value pair. The value can be a string, number, boolean, date or list, so you ask for it with inline fragments:
records { field value { ... on StringValue { s: value } ... on IntValue { i: value } ... on FloatValue { f: value } ... on BooleanValue { b: value } ... on DateValue { d: value } ... on StringArrayValue { a: value } } }Only one of s, i, f, b, d, a is present per record. Empty values come back as null.
The aggregated report (Custom Report) returns rows of dimensions and metrics, each a list of { name, value } pairs where value is always a string.
4. Click Report — individual clicks
Use this to get raw click logs: click ID, time, offer, partner, sub IDs, device, geo, IP and so on.
4.1 Discover the available columns
The Click Report's columns can differ per platform, so ask the API for the list:
query { dataReportFields(type: "Click") { name # the value you pass in `fields`, `filters` and `sort` label # the column title shown in the dashboard type # TEXT | NUMBER | BOOLEAN | DATE defaultable } }Commonly used columns:
Field name | Column in the dashboard |
| Click ID |
| Click Time |
| Offer ID |
| Offer Name |
| Advertiser Name |
| Publisher ID |
| Publisher Name |
| Publisher Sub ID |
| Publisher Sub Sub ID |
| Publisher Click ID |
| Publisher Unique 1–9 |
| User Geo Country |
| User Connection IP |
| User Device OS |
| User Device Model |
| Device advertising IDs |
| Redirect URL |
4.2 Query clicks
query Clicks( $start: LocalDateTime $end: LocalDateTime $fields: [String!] $filters: [DataReportFilter!] $first: Int $offset: Int ) { dataReport( type: "Click" start: $start end: $end fields: $fields filters: $filters first: $first offset: $offset sort: { field: "TIME", order: DESC } ) { totalCount csvDownloadUrl edges { node { records { field value { ... on StringValue { s: value } ... on IntValue { i: value } ... on FloatValue { f: value } ... on BooleanValue { b: value } ... on DateValue { d: value } ... on StringArrayValue { a: value } } } } } } }Variables — all clicks for offer 1234 from partner 56 on 1 October, 100 at a time:
{ "start": "2026-10-01T00:00:00", "end": "2026-10-01T23:59:59", "fields": ["ID", "TIME", "MESSAGE_OFFER_ID", "MESSAGE_PUBLISHER_ID", "MESSAGE_PUBLISHER_SUB_ID", "MESSAGE_USER_GEO_COUNTRY", "MESSAGE_USER_CONNECTION_IP", "MESSAGE_USER_DEVICE_OS"], "filters": [ { "field": "MESSAGE_OFFER_ID", "operator": "IN", "values": ["1234"] }, { "field": "MESSAGE_PUBLISHER_ID", "operator": "IN", "values": ["56"] } ], "first": 100, "offset": 0 }4.3 Parameters
Parameter | Required | Notes |
| yes |
|
| no | Time range. If omitted, the last 7 days are used. |
| no | Columns to return. Use names from |
| no | List of |
| no | Page size (default |
| no | Rows to skip, for paging. |
| no |
|
| no | Columns for the CSV download, if different from |
4.4 Example response
{ "data": { "dataReport": { "totalCount": 18342, "csvDownloadUrl": "/download/data-report/Click/eyJhbGciOi...", "edges": [ { "node": { "records": [ { "field": "ID", "value": { "s": "a1b2c3d4-..." } }, { "field": "TIME", "value": { "d": "2026-10-01T23:59:41" } }, { "field": "MESSAGE_OFFER_ID", "value": { "s": "1234" } }, { "field": "MESSAGE_PUBLISHER_ID", "value": { "s": "56" } }, { "field": "MESSAGE_PUBLISHER_SUB_ID", "value": { "s": "campaign_a" } }, { "field": "MESSAGE_USER_GEO_COUNTRY", "value": { "s": "DE" } } ] } } ] } } }totalCount is the number of matching clicks across all pages.
5. Conversion Report — individual conversions
Use this to get every conversion (install, registration, purchase, …) with its status, payout, the click it was attributed to, and the advertiser's postback data.
5.1 Query conversions
query Conversions( $start: LocalDateTime! $end: LocalDateTime! $fields: [ConversionReportField!] $filters: [ConversionReportFilter!] $first: Int $offset: Int ) { conversionReport( start: $start end: $end fields: $fields filters: $filters first: $first offset: $offset sort: { field: POSTBACK_TIME, order: DESC } ) { totalCount csvDownloadUrl edges { node { records { field value { ... on StringValue { s: value } ... on IntValue { i: value } ... on FloatValue { f: value } ... on BooleanValue { b: value } ... on DateValue { d: value } ... on StringArrayValue { a: value } } } } } } }Variables — approved conversions for offer 1234 in October:
{ "start": "2026-10-01T00:00:00", "end": "2026-10-31T23:59:59", "fields": [ "POSTBACK_ID", "POSTBACK_TIME", "CLICK_ID", "CLICK_TIME", "OFFER_ID", "OFFER_NAME", "OFFER_EVENT_NAME", "PUBLISHER_ID", "PUBLISHER_NAME", "PUBLISHER_SUB_ID", "EVALUATION_STATUS", "OFFER_WE_GET", "OFFER_THEY_GET", "CLICK_USER_GEO_COUNTRY" ], "filters": [ { "field": "OFFER_ID", "operator": "IN", "values": ["1234"] }, { "field": "EVALUATION_STATUS", "operator": "IN", "values": ["APPROVED"] } ], "first": 500, "offset": 0 }5.2 Parameters
Parameter | Required | Notes |
| yes | Conversion time range ( |
| no | Columns to return ( |
| no | List of |
| no | Paging. |
| no |
|
| no | Columns for the CSV download, if different from |
| no | Shortcuts to filter by app. |
5.3 Most useful fields
Conversion
Field | Meaning |
| Unique conversion ID |
| When the conversion happened |
| Which event (install, purchase, …) |
| Status, e.g. |
| Rules that changed the status |
| Whether the partner was paid / notified |
| Click-to-install time |
| Whether the conversion was later reversed |
| Whether it was edited after the fact |
Money (see section 8 for units)
Field | Meaning |
| Revenue: what the advertiser pays you |
| Payout: what you pay the partner |
| Currency of the amounts |
| Sale amount reported by the advertiser/MMP |
Who and where
Field | Meaning |
| Offer |
| Advertiser |
| Partner (publisher) |
| Partner tracking parameters from the click |
| The partner's own click ID |
The originating click
Field | Meaning |
| The attributed click |
| Geo and IP at click time |
| Device at click time |
| Advertising IDs |
Advertiser / MMP data
Field | Meaning |
| Event name and payload as sent by the advertiser |
| Install time reported by the MMP |
| The raw postback received |
| The postback we sent to the partner, and their response |
The full list is the ConversionReportField enum in the schema (over 200 fields).
6. Custom Report — aggregated statistics
The Custom Report is a pivot table: you choose dimensions (what to group by), metrics (what to count or sum) and a time range. It is the best way to get totals such as clicks, conversions, revenue, payout and profit per day, offer, partner, country, and so on.
It is called rawReport in the API.
6.1 Query
query CustomReport($q: RawQueryInput!) { rawReport(rawQuery: $q) { results { dimensions { name value } metrics { name value } } totals { name value } downloadToken } }6.2 The query input
input RawQueryInput { timeRange: RawDateRangeInput! # required dimensions: [RawStatsDimension!]! # required, can be [] metrics: [RawStatsMetric!]! # required pagination: RawQueryPaginationInput! # required: { limit, offset } sort: RawQuerySortInput! # required filters: [RawFilterClauseInput!] # optional: dimension IN values metricFilters: [RawMetricFilterClauseInput!] # optional: metric thresholds }Time range
"timeRange": { "start": "2026-10-01T00:00:00", "end": "2026-10-31T23:59:59", "granularity": "DAY" }granularity sets how rows are bucketed by time when you include the DATE dimension:
Granularity | Result |
| One row per period |
| One total for the whole range |
Sort — by a metric or a dimension:
"sort": { "field": "CLICKS", "direction": "DESC" } "sort": { "dimension": "DATE", "direction": "ASC" }Filters — keep only rows where a dimension has one of the given values:
"filters": [ { "dimension": "OFFER_ID", "value": ["1234", "5678"] }, { "dimension": "USER_GEO_COUNTRY", "value": ["US", "GB"] } ]Different filters are combined with AND; values inside one filter with OR.
Metric filters — keep only rows whose metric meets a threshold:
"metricFilters": [ { "metric": "CLICKS", "operator": "GTE", "values": [1000] }, { "metric": "ACT_WE_GET", "operator": "BETWEEN", "values": [100, 5000] } ]Operators: GTE, LTE, EQUALS, BETWEEN.
6.3 Dimensions
Group | Dimensions |
Time |
|
Time of click (for conversions) |
|
Offer |
|
Event |
|
Advertiser |
|
Partner |
|
Partner parameters |
|
Geo |
|
Device |
|
Connection |
|
Conversion outcome |
|
The full list is the RawStatsDimension enum.
6.4 Metrics
Traffic
Metric | Meaning |
| Tracked impressions |
| Tracked (accepted) clicks |
| Unique clicks |
| Traffic blocked by targeting or fraud rules |
| Clicks ÷ impressions |
Conversions
Metric | Meaning |
| All conversions, any status |
| Approved conversions (advertiser side) |
| Conversions reported to partners |
| Approved conversions ÷ clicks |
| Partner conversions ÷ clicks |
| Conversions reversed afterwards |
Money
Metric | Meaning |
| Revenue from approved conversions |
| Payout (cost) for approved conversions |
| Payout on conversions reported to partners |
| Revenue / payout across all statuses |
| Revenue − payout |
| Revenue ÷ clicks (EPC) |
| Effective cost per action |
| Sale amounts reported by advertisers |
| Money on reversed conversions |
Caps (current configuration, useful next to OFFER_ID / PUBLISHER_ID)
DAILY_CONVERSION_CAP, DAILY_REVENUE_CAP, DAILY_CLICK_CAP, MONTHLY_CONVERSION_CAP, MONTHLY_REVENUE_CAP, MONTHLY_CLICK_CAP
The full list is the RawStatsMetric enum.
6.5 Example: daily performance per offer
{ "q": { "timeRange": { "start": "2026-10-01T00:00:00", "end": "2026-10-07T23:59:59", "granularity": "DAY" }, "dimensions": ["DATE", "OFFER_ID", "OFFER_NAME"], "metrics": ["CLICKS", "ACT_CONVERSIONS", "CONVERSION_RATE", "ACT_WE_GET", "ACT_THEY_GET", "PROFIT"], "pagination": { "limit": 1000, "offset": 0 }, "sort": { "dimension": "DATE", "direction": "ASC" } } }Response:
{ "data": { "rawReport": { "results": [ { "dimensions": [ { "name": "DATE", "value": "2026-10-01" }, { "name": "OFFER_ID", "value": "1234" }, { "name": "OFFER_NAME", "value": "Example Game - US" } ], "metrics": [ { "name": "CLICKS", "value": "15230" }, { "name": "ACT_CONVERSIONS", "value": "412" }, { "name": "CONVERSION_RATE", "value": "0.027" }, { "name": "ACT_WE_GET", "value": "824.00" }, { "name": "ACT_THEY_GET", "value": "618.00" }, { "name": "PROFIT", "value": "206.00" } ] } ], "totals": [ { "name": "CLICKS", "value": "98411" }, { "name": "ACT_CONVERSIONS", "value": "2650" } ], "downloadToken": "eyJhbGciOi..." } } }totals holds the totals over all rows, not just the current page.
6.6 More examples
Payout per partner for last month (one row per partner)
{ "q": { "timeRange": { "start": "2026-09-01T00:00:00", "end": "2026-09-30T23:59:59", "granularity": "ALL" }, "dimensions": ["PUBLISHER_ID", "PUBLISHER_NAME"], "metrics": ["CLICKS", "PUB_CONVERSIONS", "PUB_THEY_GET"], "pagination": { "limit": 500, "offset": 0 }, "sort": { "field": "PUB_THEY_GET", "direction": "DESC" } } }Hourly clicks and conversions for one offer, by country
{ "q": { "timeRange": { "start": "2026-10-15T00:00:00", "end": "2026-10-15T23:59:59", "granularity": "HOUR" }, "dimensions": ["DATE", "USER_GEO_COUNTRY"], "metrics": ["CLICKS", "ACT_CONVERSIONS"], "filters": [{ "dimension": "OFFER_ID", "value": ["1234"] }], "pagination": { "limit": 5000, "offset": 0 }, "sort": { "dimension": "DATE", "direction": "ASC" } } }Sub IDs of one partner with at least 500 clicks but no conversions
{ "q": { "timeRange": { "start": "2026-10-01T00:00:00", "end": "2026-10-15T23:59:59", "granularity": "ALL" }, "dimensions": ["PUBLISHER_SUB_ID"], "metrics": ["CLICKS", "ACT_CONVERSIONS"], "filters": [{ "dimension": "PUBLISHER_ID", "value": ["56"] }], "metricFilters": [ { "metric": "CLICKS", "operator": "GTE", "values": [500] }, { "metric": "ACT_CONVERSIONS", "operator": "EQUALS", "values": [0] } ], "pagination": { "limit": 1000, "offset": 0 }, "sort": { "field": "CLICKS", "direction": "DESC" } } }Conversions by status and rejection reason
{ "q": { "timeRange": { "start": "2026-10-01T00:00:00", "end": "2026-10-15T23:59:59", "granularity": "ALL" }, "dimensions": ["POSTBACK_STATUS", "REJECTION_REASON"], "metrics": ["TOTAL_CONVERSIONS"], "pagination": { "limit": 100, "offset": 0 }, "sort": { "field": "TOTAL_CONVERSIONS", "direction": "DESC" } } }
7. Downloading reports as CSV
Every report response includes a ready-made download link for the same query, without the page limit. This is the easiest way to export large data sets.
Report | Response field | Download URL |
Click Report |
|
|
Conversion Report |
|
|
Custom Report |
|
|
curl -s -o clicks.csv "https://api.<your-platform>.swaarm-clients.com/download/data-report/Click/eyJhbGciOi..."
Good to know:
No
Authorizationheader is needed. The link itself is signed. Treat it like a password and do not share it.Links expire after 1 hour. Run the query again to get a fresh link.
To make the request cheap, ask for a page size of
1(first: 1orlimit: 1) and just use the link.Use
csvFields(Click and Conversion Reports) to put more columns in the CSV than you fetch through GraphQL.In the CSV, dates are in your platform's time zone and money is in currency units (e.g.
1.25).
8. Dates, time zones and money
8.1 Date format
Send dates as LocalDateTime strings, without a time zone:
2026-10-01T00:00:00
Both start and end are inclusive. To cover a full day, use T00:00:00 to T23:59:59.
8.2 Time zone
Time ranges and the dates in the results use your platform's reporting time zone, as configured in the Swaarm platform settings. This is the same time zone you see in the dashboard, so API numbers match the UI.
8.3 Money values
Report | How money is returned |
Custom Report ( | Strings in currency units, e.g. |
Conversion Report ( |
|
CSV downloads | Currency units. |
When you filter the Conversion Report on a money field, pass plain currency units ("1.25"), not micro-units.
Amounts are in your platform currency unless a *_CURRENCY field says otherwise.
8.4 Filter operators (Click and Conversion Reports)
Operator |
| Meaning |
| one or more | Field equals any of the values |
| one or more | Field equals none of the values |
| one | Field contains the text |
| one | Field does not contain the text |
| one | ≥ value |
| one | ≤ value |
| two | Between the two values, inclusive |
| none | Field has no value |
| none | Field has a value |
9. Complete scripts
9.1 Bash: daily custom report to the terminal
#!/usr/bin/env bash set -euo pipefail API="https://api.<your-platform>.swaarm-clients.com/graphql" TOKEN=$(curl -s "$API" -H 'Content-Type: application/json' -d "$(jq -n \ --arg u "$SWAARM_USER" --arg p "$SWAARM_PASSWORD" \ '{query: "mutation($i: AuthLoginInput!){ login(input:$i){ token { accessToken } } }", variables: {i: {username: $u, password: $p}}}')" \ | jq -r '.data.login.token.accessToken') curl -s "$API" \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "query": "query($q: RawQueryInput!){ rawReport(rawQuery:$q){ results { dimensions{name value} metrics{name value} } totals{name value} } }", "variables": { "q": { "timeRange": {"start":"2026-10-01T00:00:00","end":"2026-10-07T23:59:59","granularity":"DAY"}, "dimensions": ["DATE","OFFER_ID","OFFER_NAME"], "metrics": ["CLICKS","ACT_CONVERSIONS","ACT_WE_GET","ACT_THEY_GET","PROFIT"], "pagination": {"limit":1000,"offset":0}, "sort": {"dimension":"DATE","direction":"ASC"} } } }' \ | jq -r '.data.rawReport.results[] | [(.dimensions[].value), (.metrics[].value)] | @tsv'9.2 Python: page through all conversions
import os import requests API = "https://api.<your-platform>.swaarm-clients.com/graphql" MICRO = 100_000_000 MONEY_FIELDS = {"OFFER_WE_GET", "OFFER_THEY_GET", "OFFER_ORIGINAL_WE_GET", "OFFER_ORIGINAL_THEY_GET"} LOGIN = """ mutation($i: AuthLoginInput!) { login(input: $i) { token { accessToken } } } """ CONVERSIONS = """ query($start: LocalDateTime!, $end: LocalDateTime!, $fields: [ConversionReportField!], $filters: [ConversionReportFilter!], $first: Int, $offset: Int) { conversionReport(start: $start, end: $end, fields: $fields, filters: $filters, first: $first, offset: $offset, sort: { field: POSTBACK_TIME, order: ASC }) { totalCount edges { node { records { field value { ... on StringValue { s: value } ... on IntValue { i: value } ... on FloatValue { f: value } ... on BooleanValue { b: value } ... on DateValue { d: value } ... on StringArrayValue { a: value } } } } } } } """ def gql(query, variables, token=None): headers = {"Authorization": f"Bearer {token}"} if token else {} r = requests.post(API, json={"query": query, "variables": variables}, headers=headers, timeout=300) r.raise_for_status() body = r.json() if body.get("errors"): raise RuntimeError(body["errors"]) return body["data"] def flatten(record): value = record["value"] if value is None: return None v = next(iter(value.values())) if record["field"] in MONEY_FIELDS and v is not None: return v / MICRO return v token = gql(LOGIN, {"i": {"username": os.environ["SWAARM_USER"], "password": os.environ["SWAARM_PASSWORD"]}})["login"]["token"]["accessToken"] page_size, offset, rows = 1000, 0, [] while True: data = gql(CONVERSIONS, { "start": "2026-10-01T00:00:00", "end": "2026-10-31T23:59:59", "fields": ["POSTBACK_ID", "POSTBACK_TIME", "CLICK_ID", "OFFER_ID", "PUBLISHER_ID", "PUBLISHER_SUB_ID", "EVALUATION_STATUS", "OFFER_WE_GET", "OFFER_THEY_GET"], "filters": [{"field": "EVALUATION_STATUS", "operator": "IN", "values": ["APPROVED"]}], "first": page_size, "offset": offset, }, token)["conversionReport"] batch = [{r["field"]: flatten(r) for r in e["node"]["records"]} for e in data["edges"] or []] rows.extend(batch) offset += page_size if not batch or offset >= data["totalCount"]: break print(f"Fetched {len(rows)} conversions")
10. Troubleshooting and best practices
Common errors
Symptom | Cause and fix |
| Access token missing or expired. Refresh it (section 2.3) or log in again. |
"Access denied" on a report | The user lacks permission for that report. Ask your platform admin to grant access. |
"Unable to generate conversion report" / "Unable to generate data report" | Usually an unknown field name or an invalid filter value (e.g. text in a money filter). Check names against |
"The report would have returned more than 10GB of data…" | The Custom Report is too large. Shorten the time range, remove high-cardinality dimensions (IP, sub IDs, device model) or add filters. |
Validation error mentioning an enum | A dimension, metric or field name is misspelled. Names are case-sensitive and use |
Download link returns an error | The link is older than 1 hour. Run the query again for a fresh one. |
Best practices
Pick the right report. Need totals? Use the Custom Report. It is much faster than fetching individual clicks or conversions and adding them up yourself.
Use
granularity: ALLwhen you only need one total per group.Ask only for what you need. Fewer dimensions and fields mean faster queries.
Page through row-level reports with
first/offset, or use the CSV link for full exports.Avoid wide time ranges on click logs. Click volumes can be very large; query a day (or an hour) at a time and loop.
Re-sync recent data. Conversions can be approved, rejected or reversed after they arrive. When you sync daily, re-pull the last few days rather than only "yesterday".
Reuse your token. Log in once and refresh the token as needed; do not log in on every request.
Need help?
Contact your Swaarm account manager or our support team with the query you sent, the time you sent it, and the error you received.
