Public API and MCP server
Authentication
Two independent mechanisms:
- Bearer API token (
Authorization: Bearer <token>): for all of/api/v1/*. - Admin panel session cookie (
it_session): the only way to call the token endpoints (GET/POST /api/v1/tokens,DELETE /api/v1/tokens/{id}), because before the first token is issued there is no token to authorize with.
Issuing a token
# 1. sign in, the cookie is saved to cookies.txt
curl -s -c cookies.txt -X POST https://<tracker>/api/internal/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"<password>"}'
# 2. issue a token under this session
curl -s -b cookies.txt -X POST https://<tracker>/api/v1/tokens \
-H 'Content-Type: application/json' \
-d '{"name":"my-integration","scopes":["read","write"],"rate_limit":600}'
The value of the token field in the response is returned once; after that only its
hash is stored in the database. A token can have an expiry (expires_at) and its own rate
limit (requests per minute, default 600). The admin role can issue a token on behalf of
another user (user_id).
Revoking: DELETE /api/v1/tokens/{id} (also under the session cookie).
Permissions (scopes)
read: required for everyGET.write: required for all other methods (POST/PATCH/DELETE).mcp: a separate scope for tokens you issue for the MCP server (see below); it is a label only and grants no access by itself, so use it together withread/write.clicks: a narrow scope only for the server-side clickPOST /api/v1/clicks(showcases). It grants neitherreadnorwrite. Theadminandbuyerroles can issue it,writeonlyadmin.
A token without the required scope gets 403 forbidden.
Rate limit
Counted per token, in requests per minute (rate_limit at issue time, default 600).
Exceeding it returns 429 rate_limited. A token without an explicitly set limit is not limited.
Error format
The same across the whole API:
{"error": "unauthorized", "message": "<human-readable description>"}
message is a human-readable description for a person (the server currently writes it in
Russian); program logic should rely on the machine code in error.
Machine codes: unauthorized (missing or invalid token) · forbidden (missing scope) ·
license_required (the feature is disabled by the current license, 403) · not_found · bad_request ·
validation (400, invalid list parameters: limit, sort, ids, cursor) · conflict and
in_use (409) · module_unavailable (the module is not available in this installation, 503) · unavailable
(503, a dependency is temporarily unavailable, retry) · cache_unavailable (502, a flow change
could not be published to the click path cache) · clickhouse (502) · rate_limited (429, together
with a Retry-After header) · internal. Some endpoints have codes of their own (offer_capped,
in_progress, app_not_found…): they are listed in the operation's response description in the
OpenAPI spec. In the spec this format is the components/schemas/Error schema, described by the
default response of every operation.
Lists: pagination, search, sorting
The rules are the same for all catalog lists (/offers, /flows, /sources, /sites, /advertisers,
/webhooks, /webhooks/{id}/deliveries, /manual-costs and the nested lists of a source and a site):
| Parameter | What it does |
|---|---|
limit |
Page size. Default 50; anything above 500 is cut to 500. Zero, a negative number or a non-number gives 400 validation |
offset |
Offset from the start of the result set |
q |
Search by name (case-insensitive). A number is also matched against id. A string shorter than two characters is ignored unless it is a number |
sort |
field,-field, up to three fields. A minus means descending order. Allowed fields are specific to each list; an unknown field gives 400 validation. Equal values are always ordered by id, so page order is stable |
ids |
1,2,3: only the listed records (no more than 500) |
facets |
For example status,owner: counts per value across the whole result set. A field's counts are computed without the filter on that same field |
The response has the form {"items": [...], "total": 123, "limit": 50, "offset": 0}. items is
always an array. total is null if the overall count could not be computed within the allotted time.
The same total comes in the X-Total-Count header. Older clients that send no parameters get, as
before, the first page of the previous size in items/total: 50 records for the catalog and webhook
deliveries, 500 for /webhooks, /manual-costs (its limit cap is 1000), /verticals and the nested
lists of a source and a site. Fields were only added to the response, none removed.
Logs that grow without bound are paged with a cursor: cursor instead of offset, and the response
has next_cursor (null on the last page). The cursor remembers the filters, q and sort it was
obtained with. Passing it with different parameters returns 400. offset and cursor together
also return 400.
Core entities
CRUD (list/get/create/update/delete, where applicable) for:
/api/v1/advertisers: advertisers and CPA networks;/api/v1/offers, plusPOST /offers/{id}/pauseand/resume;/api/v1/flows, plusPATCH /flows/{id}/weights(offer split weights),POST /flows/{id}/pauseand/resume;/api/v1/sources: traffic sources;/api/v1/sites: sites in your site network;/api/v1/tokens: managing your own API tokens;/api/v1/webhooks: webhook subscriptions, plus/webhooks/{id}/deliveries(delivery log).POST /api/v1/clicks: a server-side click without a redirect, for showcases (scopeclicksorwrite, idempotency byIdempotency-Key, the click is recorded withhost = "s2s").
In GET /offers and GET /offers/{id} an offer has fields for showcases: network (the
advertiser's network code), network_offer_id (the ID in the network, equal to external_id) and
cap_state (cap state, null when Redis is unavailable). Ad labeling (erid, advertiser details,
?has_erid=, ?updated_since=, POST /offers/erid-import) and showcase conditions
(conditions) are also meant for showcases.
The full schema of request and response bodies is in the OpenAPI spec, see the next section.
OpenAPI 3.1 spec
A machine-readable description of all of /api/v1/* in OpenAPI 3.1.0
(schemas are JSON Schema 2020-12):
GET /api/v1/openapi.json: JSON, for client generators, Postman/Insomnia and MCP;GET /api/v1/openapi.yaml: the same in YAML.
The spec contains every /api/v1 route (automated tests check in both directions that the spec
matches the actual routes), request and response schemas, an operationId for every operation and a
single Error schema. Internal endpoints (/api/internal/*) are not in it. Module proxies (/reports/*,
/caps/*, /rules/*, /postback/*, /finance/*, /integrations/*) are described as one operation per
method without schemas; module routes are listed further down in this document.
# download the spec
curl -s https://<tracker>/api/v1/openapi.json -o inotrack-openapi.json
# list of operations: method, path, operationId
jq -r '.paths | to_entries[] | .key as $p | .value | to_entries[]
| select(.key != "parameters") | "\(.key | ascii_upcase) \($p) \(.value.operationId)"' inotrack-openapi.json
# offer schema
jq '.components.schemas.Offer' inotrack-openapi.json
# a client, for example in TypeScript (https://openapi-generator.tech; the generator must support OpenAPI 3.1)
npx @openapitools/openapi-generator-cli generate -i inotrack-openapi.json -g typescript-fetch -o ./inotrack-client
servers in the spec is the relative /api/v1: give your client generator or Postman the base
address https://<tracker>/api/v1. The bearer token is the bearerAuth scheme; /tokens works only
under the session cookie (sessionCookie).
Both endpoints respond without a token or session: authorization is not checked on these two exact
paths. The rest of /api/v1
still requires authorization: without Authorization you get 401 unauthorized.
Reports, caps, rules, postbacks, finance, integrations: through a proxy
These six modules have no paths of their own in /api/v1 and are proxied as a whole:
/api/v1/reports/*, /api/v1/caps/*, /api/v1/rules/*, /api/v1/postback/*,
/api/v1/finance/*, /api/v1/integrations/*. If a module is not available in your
installation, the corresponding prefix responds 503 module_unavailable.
⚠️ /api/v1/integrations/* currently always responds 503, and this is a deliberate decision,
not a bug: ad platform integrations are managed from the admin panel under the session cookie
(/integrations, not under /api/v1), and there is no public API for them yet.
The proxy determines the required scope from the HTTP method (GET → read, everything else → write),
except for report module paths explicitly marked read-only (for example, CSV export): read is
enough for them even though their method is POST.
One finance module endpoint worth knowing about is GET /api/v1/finance/fx/status (admin role
only): freshness of the exchange rate source (date of the last known Bank of Russia/CoinGecko rate,
age, stale flag); see the FAQ for how the rate is fixed on the event date.
Reports (/api/v1/reports/*)
Available routes:
| Path | Method | Purpose |
|---|---|---|
/query |
POST | universal report: {dimensions, metrics, filters, date_from, date_to, timezone, currency, ...} |
/dims, /metrics |
GET | list of available dimensions and metrics |
/seo |
GET | "site → page → revenue" report |
/export.csv |
POST | streaming CSV export |
/drilldown |
POST | drill-down into details (campaign → ad set → creative → placement) |
/cohorts |
POST | cohort analysis and LTV |
/plans, /plans/{id}, /plans/fact |
POST/DELETE/GET | plan vs. actual and a linear forecast |
/shares, /shares/{id}/revoke |
POST/POST | managing secret read-only report links; they require a bearer token, like the rest of the proxy |
/shares/view/{token} |
GET | public view by link: not under /api/v1, opens without authorization (see the FAQ) |
/annotations, /annotations/{id} |
GET/POST/PATCH/DELETE | manual and automatic chart annotations |
/creatives, /creatives/{id}, /creatives/{id}/upload, /creatives/report |
GET/POST/PATCH/DELETE | creative library, per-creative metrics |
/sql |
POST | SQL sandbox, see below |
⚠️ include_bots and P&L. By default (include_bots not passed or false), /query,
/drilldown and /cohorts compute revenue and the other money metrics without
conversions with is_bot_click = 1: this is the "how much we earned from real users" figure the
user sees in reports. The financial P&L (/api/v1/finance/*) counts
revenue over all approved conversions regardless of is_bot_click: this is the full amount for
accounting and reconciliation with the network, which does not care who clicked the button. Because
of this, report revenue with the default include_bots=false and P&L revenue for the same period
do not have to match: the difference is exactly the amount of approved conversions from bot
traffic. To compare a report with the P&L like for like, request the report with include_bots=true.
SQL sandbox (POST /api/v1/reports/sql)
An arbitrary SELECT (including WITH ... SELECT) straight against ClickHouse, for the admin
role only. Safeguards:
- only one
SELECT/WITHstatement per call (an extra;inside the text is rejected); - the query runs as a separate ClickHouse user with
SELECT-only rights on the database, soINSERT/DDL/DROPare physically impossible at the permission level, not just by a text filter; - a hard execution timeout of 10 seconds;
- a hard row limit of 1000 (ClickHouse truncates the result itself,
result_overflow_mode: break) rather than returning an error when it is exceeded; - a separate small connection pool (up to 3), so a heavy ad hoc query does not take connections away from production reports;
- every query is logged (who, query text, duration, row count, success or error).
curl -s -X POST https://<tracker>/api/v1/reports/sql \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"query": "SELECT source, count() FROM clicks WHERE ts >= today() - 1 GROUP BY source ORDER BY count() DESC LIMIT 20"}'
Webhooks
POST /api/v1/webhooks creates a subscription to events: conversion, cap.reached,
rule.fired, anomaly, offer.down. The signing secret (secret) is returned once, in
the creation response.
The delivery queue is persistent (it survives a process restart): on a delivery error there is
exponential backoff (5s, 10s, 20s... capped at 1 hour) up to max_attempts (fixed at
8 attempts, not configurable through the API), then the status becomes failed. Every attempt is
visible in GET /api/v1/webhooks/{id}/deliveries (response code, error, attempt count).
The body signature is in a header:
X-Inotrack-Signature: sha256=<hex hmac-sha256(secret, request_body)>
Verify it with a constant-time comparison (hmac.Equal), not with a plain string ==.
MCP server (working with the tracker from Claude Code)
A separate program, inotrack-mcp, on top of the same public API: it does not touch any database
or Redis of its own, only HTTP to /api/v1. It runs on your machine next to Claude Code, not on
the tracker server. Ask support for a ready-made binary for your OS.
Environment variables: INOTRACK_API_TOKEN (required; a separate token with the
mcp+read scopes is recommended, plus write only if you need the write tools) and INOTRACK_API_URL
(default http://localhost:8081/api/v1).
Connecting:
claude mcp add inotrack \
--env INOTRACK_API_URL=https://<tracker>/api/v1 \
--env INOTRACK_API_TOKEN=<token> \
-- /path/to/bin/inotrack-mcp
Read tools
report_query, list_offers, list_sites, list_flows, get_anomalies,
get_finance_summary.
Write tools
pause_offer, resume_offer, set_weights, blacklist_add. They require an explicit
"confirm": true in the arguments: without it the server does not perform the action and instead
returns text describing the consequences (not a call error but a deliberate refusal). Repeat the call
with confirm: true to execute it. All write calls go with the same token through the public
API, so the tracker itself writes the audit log entry (an actor like
api-token:<token name>); the MCP server has no separate audit and does not need one.
Known MCP limitation
report_query/get_anomalies (through the reports module) and get_finance_summary (through
the finance module) return isError: true with a message about the unavailable module if that
module is not available in your installation (the API responds 503 module_unavailable). This is
expected behavior, not an MCP server error.
Next steps
Instance maintenance (backups, updates, doctor): instance maintenance. Honest answers to reliability questions: FAQ.