INOTrackfast, reliable, unapologetic

Public API and MCP server

Authentication

Two independent mechanisms:

  1. Bearer API token (Authorization: Bearer <token>): for all of /api/v1/*.
  2. 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 every GET.
  • 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 with read/write.
  • clicks: a narrow scope only for the server-side click POST /api/v1/clicks (showcases). It grants neither read nor write. The admin and buyer roles can issue it, write only admin.

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, plus POST /offers/{id}/pause and /resume;
  • /api/v1/flows, plus PATCH /flows/{id}/weights (offer split weights), POST /flows/{id}/pause and /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 (scope clicks or write, idempotency by Idempotency-Key, the click is recorded with host = "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/WITH statement per call (an extra ; inside the text is rejected);
  • the query runs as a separate ClickHouse user with SELECT-only rights on the database, so INSERT/DDL/DROP are 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.

Support