Campaign Cost API
Data connector reference

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.

GET https://api.mictapgo.com/reporting
ModelYou pull, we serve
TransportHTTPS, GET only
OutputJSON or CSV
ClockUTC, no offsets
Denominated inUSD
One row perDate × App × Campaign
Retained90 days
Suggested cadenceOnce a day, last 7 days

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.

Request header
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

ParameterRequiredFormatMeaning
start_dateYesYYYY-MM-DDEarliest UTC day to include; the day itself counts
end_dateYesYYYY-MM-DDLatest UTC day to include; the day itself counts
formatNojson | csvOmit for json
include_emptyNotrue | falseOn 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.

Example request
curl -H "Authorization: Bearer <api_key>" \
  "https://api.mictapgo.com/reporting?start_date=2026-08-07&end_date=2026-08-09"

4Payload

Singular fieldJSON keyTypeNotes
DatedateYYYY-MM-DDThe UTC day the figures belong to
App NameappstringSpelled as the advertiser's own Singular account spells it
Appsite ID / bundle IDstore_idstringThe advertised app's App Store ID on iOS, or its package name on Android
Campaign Namecampaign_namestringThe name the advertiser knows the campaign by
OSosstringEither ios or android
CurrencycurrencystringISO code the cost figure is denominated in. On every row, not just the first.
CostcostnumberSpend 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.
InstallsinstallsintegerInstall count as we recorded it
On the cost figure

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.

Installs with zero cost are genuine

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.

Response — JSON
{
  "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:

Response — CSV
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_date back to yesterday rather than refusing the call. You still get a 200, and the notices array 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=false if you would rather they were left out. Note that under this account's billing a day can also carry installs with a cost of 0 — 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 notices as the signal: if we shortened anything it is named there, and an empty notices means you received the whole window you asked for.

6Failure modes

StatusWhat it tells you
200The call worked
400Something about the parameters does not parse — a date in the wrong shape, a range running backwards, a format we do not emit
401No key was presented, or the one presented is not recognised
500Something broke at our end. Retrying is safe.
Error body
{ "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.