docs
Open the app →

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.

any dayNew endpoints, optional fields, fields in an answeranything that could break a scriptAnnouncedDeprecation header,changelog90 days or morethe endpoint keeps workingSunsetthe date in the header
Adding never breaks a script, so it happens any day. Anything that could break one is announced on the endpoint itself at least 90 days first.

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-Key on every POST.
  • 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 answers next_cursor.
  • Added: per-minute rate limits by plan, with RateLimit-* headers and 429 rate_limited.
  • Added: read-only keys, refused any change with 403 read_only_key.
  • Added: POST and PUT /offers answer group as well as group_name.
  • Added: POST /reports/exports takes bucket: weekday_hour, as the trends report does.
  • Fixed: a body that is not JSON, or an id in the wrong form, answers 400 bad_body or 400 invalid_input instead of 500.
  • Fixed: POST /campaigns refuses a status other than active or paused with 400 invalid_status instead of 500.
  • Fixed: POST /campaigns/{id}/clone keeps direct-URL destinations, and refuses a flow that no longer validates with the flow's own 400 code.
  • Changed, before the promise above applied: POST and PUT /offers refuse a country that is not two letters with 400 invalid_country — it was silently dropped. PATCH /short-links requires is_active to be true or false — left out, it used to mean true. A malformed smartlink id is 400 invalid_id on /logs/smartlink-clicks and /reports/exports (it was 404 and invalid_link). /logs/repeat-ips answers the min_hits it applied.
  • Changed: geo database settings under /geo-databases are no longer writable by accounts; they are shared by every account and managed by UTMCAP.