docs
Open the app →

Using the API

Keys, the answer format, rate limits, paging and errors — with copy-paste examples in curl, Node and Python.

one callYour scriptKey valid?Allowed?Under the limit?Your datawhat comes back{ data, error: null }with RateLimit-Remaining on every answer{ data: null, error: { code } }401 key · 402 plan · 403 read-only · 429 limitlists come 500 rows a page — follow next_cursor until it is null
Every call passes the same three checks before it reaches your data, and every answer — success or refusal — comes back in the same envelope.

The API is the same one the dashboard runs on: anything you can see or change on screen, a script can too. It is on the Growth, Pro and Scale plans.

Every endpoint, with its fields, is in the API reference. For Postman, Insomnia or a code generator, import https://app.utmcap.com/api/v1/openapi.json — it describes every call an API key can make, and a test in our build fails if it ever disagrees with the code.

#Keys

Make one under Settings → API. Send it on every call:

Authorization: Bearer utmk_…

A Read only key can read everything and is refused any change with 403 read_only_key — the one to give a spreadsheet, a BI tool or somebody else's script. Keys can expire and can be rotated; see Your account. Treat a key as a password.

#Answers

Every answer is JSON in the same envelope:

{ "data": { "rows": [] }, "error": null }

and every failure:

{ "data": null, "error": { "message": "This API key is read-only", "code": "read_only_key" } }

Branch on code; show message to a person. The codes every call can meet:

Status Code Meaning
400 bad_body, invalid_input A body that is not JSON, or a value such as an id in the wrong form
401 unauthenticated No key, or a key that is revoked or expired
402 plan_limit The plan has no API, or a plan limit was reached
403 read_only_key A read-only key tried to change something
429 rate_limited This minute's calls are used up — wait Retry-After seconds
500 internal_error Something failed on our side; a read is safe to retry

Each endpoint's own codes are listed with it in the reference.

#Rate limits

Calls are counted per account, all its keys together, in one-minute windows: 300 a minute on Growth, 600 on Pro and 1,200 on Scale. Every answer says where you stand:

RateLimit-Limit: 300
RateLimit-Remaining: 299
RateLimit-Reset: 42

RateLimit-Reset is the seconds until the minute starts again. Past the limit the answer is 429 with Retry-After.

#Retrying a create

Send an Idempotency-Key header on a POST and a retry with the same key gets the first answer back instead of creating twice. How it works, and what we promise about the API not changing, is on API stability and changes.

#Lists and paging

Lists come 500 rows at a time. Ask for fewer with ?limit= (1–500). While more remain the answer has a next_cursor; send it back as ?cursor= and stop when it is null.

#Dates

Reports take ?from= and ?to= as YYYY-MM-DD or YYYY-MM-DD HH:MM:SS, in UTC. A bare to date means the end of that day. Left out, the window is the last seven days including today.

#Examples

List your campaigns with this week's figures.

curl

curl "https://app.utmcap.com/api/v1/campaigns?from=2026-09-01&to=2026-09-07" \
  -H "Authorization: Bearer $UTMCAP_KEY"

Node (18 or later, no packages)

const key = process.env.UTMCAP_KEY;
const base = 'https://app.utmcap.com/api/v1';

async function call(path) {
  const res = await fetch(base + path, { headers: { Authorization: `Bearer ${key}` } });
  const body = await res.json();
  if (res.status === 429) {
    const wait = Number(res.headers.get('Retry-After') ?? 60);
    await new Promise((r) => setTimeout(r, wait * 1000));
    return call(path);
  }
  if (body.error) throw new Error(`${body.error.code}: ${body.error.message}`);
  return body.data;
}

// Every offer, however many pages that takes.
const offers = [];
let cursor = null;
do {
  const page = await call(`/offers${cursor ? `?cursor=${cursor}` : ''}`);
  offers.push(...page.rows);
  cursor = page.next_cursor;
} while (cursor);
console.log(offers.length, 'offers');

Python (3.8 or later, with requests)

import os, time, requests

KEY = os.environ["UTMCAP_KEY"]
BASE = "https://app.utmcap.com/api/v1"

def call(path, params=None):
    res = requests.get(BASE + path, params=params, headers={"Authorization": f"Bearer {KEY}"})
    if res.status_code == 429:
        time.sleep(int(res.headers.get("Retry-After", 60)))
        return call(path, params)
    body = res.json()
    if body["error"]:
        raise RuntimeError(f'{body["error"]["code"]}: {body["error"]["message"]}')
    return body["data"]

report = call("/reports/performance", {"dimension": "country", "from": "2026-09-01", "to": "2026-09-07"})
for row in report["rows"]:
    print(row["dims"][0], row["clicks"], row["revenue"])

#What is not in the API

Signing in, billing, the team, support conversations and managing API keys themselves need a person in the dashboard. The tag, goals fired from your pages and postbacks have their own pages: the tag, goals and postbacks.