Campaign Cost API
Daily campaign spend and install counts for a shared advertiser, aggregated and ready for Singular to collect. One pull a day; one key per account.
1How it works
One authenticated GET hands back a day-by-day breakdown of spend and installs for a single advertiser, already rolled up to date × app × campaign. Nothing is cached or paged: each call is recalculated from source, so there is no cursor to hold on to and no state that can go stale between pulls.
§4 lists the eight keys a row carries; there is no ninth. Where we hold nothing for a column, it is left out of the payload altogether — you will not receive empty strings or N/A filler to strip.
2Access
Every customer receives their own key, which is what keeps one advertiser's pull cleanly separated from another's. On our side the key is bound to that advertiser's campaigns and can reach nothing outside them.
Authorization: Bearer <api_key>
Connectors that prefer a different shape may send X-API-Key: <api_key> or append ?api_key=<api_key> instead. All three are read the same way.
Nothing has to be exchanged first. The key is long-lived and travels with each call, which makes your opening request both the handshake and the first report.
3Request
Query parameters
| Parameter | Required | Format | Meaning |
|---|---|---|---|
| start_date | Yes | YYYY-MM-DD | Earliest UTC day to include; the day itself counts |
| end_date | Yes | YYYY-MM-DD | Latest UTC day to include; the day itself counts |
| format | No | json | csv | Omit for json |
| include_empty | No | true | false | On unless you turn it off; false hides zero rows — see §5 |
Either date may also be written as start or end; both spellings resolve to the same parameter.
Any width of window is fine in a single call, so walking the range one day at a time is unnecessary. We would suggest one call per pull covering the previous week — but iterating day by day behaves identically, and there is no throttle to run into either way.
curl -H "Authorization: Bearer <api_key>" \
"https://api.mictapgo.com/reporting?start_date=2026-08-07&end_date=2026-08-09"
4Payload
| Singular field | JSON key | Type | Notes |
|---|---|---|---|
| Date | date | YYYY-MM-DD | The UTC day the figures belong to |
| App Name | app | string | Spelled as the advertiser's own Singular account spells it |
| Appsite ID / bundle ID | store_id | string | The advertised app's App Store ID on iOS, or its package name on Android |
| Campaign Name | campaign_name | string | The name the advertiser knows the campaign by |
| OS | os | string | Either ios or android |
| Currency | currency | string | ISO code the cost figure is denominated in. On every row, not just the first. |
| Cost | cost | number | Spend for the whole row, to 2 dp — an aggregate, never a unit rate. Serialised as a JSON number, which drops trailing zeros: expect 184.8 where a string would have read 184.80. |
| Installs | installs | integer | Install count as we recorded it |
What lands in cost is everything spent on that date × app × campaign combination — already summed. Divide it by the installs you attributed to reach an effective CPI; we are supplying the numerator, so dividing what we send by anything on our side would double the operation.
cost reflects what the advertiser is actually billed under their commercial terms — and this account is not billed per install. Spend accrues on post-install conversion events (purchases), so a row can legitimately show installs alongside a cost of 0: users installed that day, and the billable purchases landed on other days or not at all. The example below includes exactly such a day (2026-08-09: 46 installs, cost 0).
Two consequences for validation on your side: do not treat install-bearing, zero-cost rows as gaps in the feed, and do not expect cost ÷ installs to resemble a per-install rate — it is an effective figure and will move day to day.
{
"status": "ok",
"start_date": "2026-08-07",
"end_date": "2026-08-09",
"timezone": "UTC",
"row_count": 3,
"rows": [
{
"date": "2026-08-07",
"app": "Possible",
"store_id": "1380384597",
"campaign_name": "POSSIBLE_IOS_US",
"os": "ios",
"currency": "USD",
"cost": 75,
"installs": 39
},
{
"date": "2026-08-08",
"app": "Possible",
"store_id": "1380384597",
"campaign_name": "POSSIBLE_IOS_US",
"os": "ios",
"currency": "USD",
"cost": 75,
"installs": 43
},
{
"date": "2026-08-09",
"app": "Possible",
"store_id": "1380384597",
"campaign_name": "POSSIBLE_IOS_US",
"os": "ios",
"currency": "USD",
"cost": 0,
"installs": 46
}
]
}
CSV
Append &format=csv to get the very same rows in the very same sequence, headed with the column names your reporting template uses:
Date,App,Store ID,Campaign Name,OS,Currency,Cost,Installs
2026-08-07,Possible,1380384597,POSSIBLE_IOS_US,ios,USD,75,39
2026-08-08,Possible,1380384597,POSSIBLE_IOS_US,ios,USD,75,43
2026-08-09,Possible,1380384597,POSSIBLE_IOS_US,ios,USD,0,46
There is nowhere in a CSV to put an envelope, so when a window has been adjusted the explanation travels in an X-Report-Notices response header rather than the notices field.
5Timing and retention
- A job at 00:00 UTC closes the books on the day that has just ended. Anything earlier than that moment is settled, which makes yesterday the newest day available to you.
- Ask for something more recent and we quietly pull
end_dateback to yesterday rather than refusing the call. You still get a200, and thenoticesarray on the response states exactly what we moved. - We hold 90 days, comfortably enough for a first-connect backfill in one go. Reaching past that edge is not an error either — we start from the oldest day we have and record the fact in
notices. - Numbers for a given day can shift slightly for a while afterwards, as conversions arrive late. This needs nothing from you: a routine trailing-week re-pull sweeps up the corrections by itself.
- Quiet days still produce a row. A campaign that was running and simply earned nothing comes back as zeros. An absent row carries a different meaning entirely — that the campaign did not exist that day. Send
&include_empty=falseif you would rather they were left out. Note that under this account's billing a day can also carry installs with a cost of0— that is a real day, not a gap; see the note in §4. - Getting back fewer rows than days is normal and not a truncation. The count tracks how long the campaign itself has been running, not how wide your window was, so a campaign started last week answers a 90-day request with roughly seven rows — and that response is complete. Trust
noticesas the signal: if we shortened anything it is named there, and an emptynoticesmeans you received the whole window you asked for.
6Failure modes
| Status | What it tells you |
|---|---|
| 200 | The call worked |
| 400 | Something about the parameters does not parse — a date in the wrong shape, a range running backwards, a format we do not emit |
| 401 | No key was presented, or the one presented is not recognised |
| 500 | Something broke at our end. Retrying is safe. |
{ "status": "error", "error": "<description>" }
Do not read 200 with an empty rows array as a failure. It says only that nothing existed to report inside that window, and it is a perfectly ordinary answer. Where a campaign did exist but earned nothing, you will see it as zeros instead of an absence — see §5.