API stability and changes
What we promise about the API not changing under you, how a change is announced, retrying safely with Idempotency-Key, and every change so far.
A script written against the API today should still work next year. This page is the promise that makes that true, and the record of every change.
#The promise
Everything under https://app.utmcap.com/api/v1 follows these rules.
Added freely, at any time
- New endpoints.
- New optional parameters and body fields.
- New fields in an answer. Read the fields you need and ignore the rest.
- New error codes on a request that was already failing.
Never without 90 days' notice
- Removing or renaming an endpoint, a parameter or a field.
- Changing what a field means, or its type.
- Making an optional parameter required.
- Refusing a request that works today.
When one of those is coming, the endpoint answers with two headers for at least 90 days before it happens:
Deprecation: @1789516800
Sunset: Tue, 15 Dec 2026 00:00:00 GMT
Link: <https://docs.utmcap.com/api/changes/>; rel="deprecation"
Deprecation is when it was announced, Sunset the first day it may stop
working. It is also marked deprecated in the
OpenAPI file and listed below.
The one exception is security. If an endpoint lets one account reach another's data, it is fixed the same day, and noted here afterwards.
A change that cannot keep these rules goes into a new version, /api/v2,
with /api/v1 kept alongside it for at least a year.
#Retrying safely
A create that times out leaves you not knowing whether it happened. Send an
Idempotency-Key header — any unique value, such as a UUID — and retry with
the same key:
curl -X POST "https://app.utmcap.com/api/v1/offers" \
-H "Authorization: Bearer $UTMCAP_KEY" \
-H "Idempotency-Key: 6f1c2a4e-9b7d-4c3e-8a21-5d0f7e9b3c11" \
-H "Content-Type: application/json" \
-d '{ "name": "Spring offer", "url": "https://example.com/?c={click_id}" }'
- The same key and the same body within 24 hours gets the first answer back,
with
Idempotent-Replayed: true, and nothing is created twice. - The same key while the first request is still running:
409 idempotency_in_progress. - The same key with a different body:
422 idempotency_key_reused. - If the first attempt failed on our side (a 5xx), the retry runs again.
Every POST accepts it. PUT, PATCH and DELETE already come out the same
however often they are sent.
#Changes
16 September 2026
- Added: webhooks for conversions, broken and unsafe links, paused campaigns and plan limits — signed, retried for a day, with a delivery log.
- Added:
Idempotency-Keyon everyPOST. - Added:
/geo-database— connect your own paid geo lookup provider (Growth and up). - Added:
/api/v1/openapi.json, describing every endpoint in this reference. - Added: API keys can expire (30, 90 or 365 days) and be rotated with a day's overlap.
- Added:
?limit=and?cursor=on every list, which answersnext_cursor. - Added: per-minute rate limits by plan, with
RateLimit-*headers and429 rate_limited. - Added: read-only keys, refused any change with
403 read_only_key. - Added:
POSTandPUT /offersanswergroupas well asgroup_name. - Added:
POST /reports/exportstakesbucket: weekday_hour, as the trends report does. - Fixed: a body that is not JSON, or an id in the wrong form, answers
400 bad_bodyor400 invalid_inputinstead of500. - Fixed:
POST /campaignsrefuses astatusother thanactiveorpausedwith400 invalid_statusinstead of500. - Fixed:
POST /campaigns/{id}/clonekeeps direct-URL destinations, and refuses a flow that no longer validates with the flow's own400code. - Changed, before the promise above applied:
POSTandPUT /offersrefuse acountrythat is not two letters with400 invalid_country— it was silently dropped.PATCH /short-linksrequiresis_activeto betrueorfalse— left out, it used to meantrue. A malformed smartlink id is400 invalid_idon/logs/smartlink-clicksand/reports/exports(it was404andinvalid_link)./logs/repeat-ipsanswers themin_hitsit applied. - Changed: geo database settings under
/geo-databasesare no longer writable by accounts; they are shared by every account and managed by UTMCAP.