Routy

API

The REST API the Routy dashboard itself calls, with an OpenAPI document, API-key auth and `/v1` paths. Issue a token from your account settings and the endpoints behind the screens are the endpoints you call.

What this feature does

The Routy dashboard is a client of the Routy API. The screens you use call the same /v1 endpoints with the same auth, the same filters and the same paging you get, so a capability doesn't sit in the UI for a release or two before it reaches the API.

Authentication is an API key in the Authorization header, under the ApiKey scheme rather than Bearer:

Authorization: ApiKey <your-token>

Paths sit under /api/v1 on the Routy host, so a call reads https://platform.routy.app/api/v1/accounts. Responses are JSON in an envelope carrying code, message and data, with a pagination object alongside data on a paged response. List endpoints page with offset (0-based) and limit, and that holds across resources, so the client you write for accounts behaves the same against clicks or exports.

A key belongs to your account rather than to the person who created it. It keeps working when its creator's user is disabled, and audit history shows the key's name instead of a person's.

What you'll get out of it

  • Token management at /v1/api-tokens: POST to create, GET to list or fetch one, DELETE /{id} to revoke. The raw secret comes back once, in the create response, and no endpoint returns it again. A token name has to be at least 4 characters and unique on the account, so a list of ten keys stays readable.
  • An expiry you can't forget about. Expiry defaults to a year out and can't be set further than a year. A job at 08:00 UTC raises an issue on your account 7 days before a key expires, escalates it inside the last day, and marks it critical once the key has stopped working. The issue carries the key's name, its expiry date and when it was last used, which is usually enough to tell a live integration from one you abandoned.
  • An OpenAPI document for the client surface at /api/swagger/v1/swagger.json, with Swagger UI at /api/swagger and a Scalar reference at /api/docs. Feed the document to openapi-generator and you get a typed client in your language. Both auth schemes are declared in it, so the generated client knows about ApiKey as well as bearer JWT.
  • A performance report built for callers, not for the UI. GET /v1/reports/events/performance takes breakdowns in fields, conversion metrics in stats, and a range in conversionDateFrom and conversionDateTo. limit runs from 1 to 999 and defaults to 100. responseType=table gives you columns plus arrays of values, which is what you want for a grid or a CSV; responseType=json gives keyed objects, which is what you want for a chart. Malformed input returns 400 naming the unknown field or metric rather than a 500.
  • Accounts with their links and their numbers in one call. GET /v1/composites/accounts/metrics returns a page of accounts, each carrying a metrics block for the window in metricsFrom and metricsTo, plus a comparisonMetrics block when you also pass comparisonFrom and comparisonTo. sortBy accepts clicks, signups, firstTimeDeposit (alias ftd), qualifiers, cpaCommission, revShareCommission, commission and flatFeeCommission, and the sort runs over the whole filtered set before paging. A metric sort defaults to highest first; prefix the column with + for ascending, as in sortBy=+clicks. The same arrangement exists one level down at /v1/composites/accounts/links/metrics.
  • Usage figures for your own keys over a rolling 72 hours. GET /v1/traffic-controls/api-usage/stats totals calls, 429 rejections, unique IPs and counts by status class. /api-usage/timeseries buckets calls by hour or by day. /api-usage/breakdown groups them by endpoint or by API key, which is how you find out which of your scripts is responsible for a spike.
  • Two endpoints for checking what's running. GET /active-version returns the deployed build: version, build number, build time and commit sha. GET /api/health is unauthenticated and meant for uptime checks.

Three things the API doesn't give you today. Scopes recorded on a token don't yet narrow what that token can reach, so treat a key as account-wide access and issue one per integration you can revoke on its own. There's no OAuth authorisation server, so you can't build an app that asks another Routy customer to grant it access; each customer issues their own key. And event exports only go back 120 days, so a warehouse backfill can't start from the API alone.

How it actually works

Issuing a token

Creating a token needs account-admin permission and the api_tokens entitlement, which is on by default and can be switched off by a subscription. Without either, every token endpoint returns 403.

POST /v1/api-tokens takes a name, an optional expirationDate and an optional scopes array. GET /v1/api-tokens/scopes returns the scope strings you're allowed to grant, which is Routy's full permission set intersected with the permissions you hold yourself. A scope you don't hold is refused with 403; a scope string that isn't a real permission is refused with 400 rather than stored, so a typo can't become a permission later when that name exists.

The response carries id, expirationDate and token. token is base64 of 32 random bytes, so it can contain +, / and =. Treat it as opaque, JSON-escape it when you write it into a config file, and never put it in a query string.

Revoking is DELETE /v1/api-tokens/{id}, and it returns 200 whether or not the id existed, so a retry is harmless. Validated keys are cached for 30 seconds, so a revoked key can keep working for up to half a minute. Don't build a confirmation screen that promises an instant cut-off.

What a token can reach

A scope you set is stored on the token and shows in the listing. It does not yet restrict the token: a key is currently also granted account-admin rights alongside its scopes, so a key created with reports.read can still reach admin endpoints. So a key sitting in a config file on a laptop can create further keys, change account settings and read every report on the account. Name each key for the integration that holds it, give it the shortest expiry that integration can live with, and revoke it rather than reusing it.

Keys created before scopes existed carry no scopes at all. Those are marked as legacy and are refused on the MCP endpoint, since there's no way to tell what they were meant to do.

Pointing an AI assistant at your reports

Routy serves an MCP endpoint over streamable HTTP at POST /mcp, reachable at mcp.routy.app, authenticated with an ordinary API key in the same Authorization: ApiKey <token> header. It exposes three tools: list_reports, describe_report and run_report. A Claude Desktop or Claude Code config block is the server url, the transport and that header.

The endpoint requires a key carrying reports.read or reports.update, and refuses legacy keys with no scopes. What an assistant can do through it is bounded by those three tools, which read reports and nothing else.

GET /v1/link-generator/router/brand-link/{brandLinkId}/traffic-source/{trafficSourceId}/url builds the tracking URL you publish. You can now hand it an affiliate tracker's name instead of its id, which is what you want when your links come out of a spreadsheet or an ad platform macro. A name is 4 to 50 characters; a value that's only digits is read as an id, so write y2026 rather than 2026 if you mean a name. A name Routy already knows is swapped for its id. A new one becomes a tracker on the first click that carries it.

The tracker is checked before you get the link back. An id that doesn't exist or belongs to another account is refused with the reason, and so is a name outside 4 to 50 characters. If your own tooling has been sending a tracker id that isn't yours, it will start getting an error where it used to get a link. That link was already broken for visitors, so the error arrives earlier rather than newly. Generated links come back lower-cased, so a name is matched however you capitalise it.

Checking what's up

GET /api/health needs no auth and reports four components: api, database, queue and search. It returns 503 only when api or database is down. A queue or search problem reports degraded with a 200, so your pager doesn't fire for a dependency that isn't blocking calls. The report is cached for a few seconds, so polling it hard won't put load on the database.

Operators get GET /api/health/detailed, which lists every registered check with its duration and error text. It's gated by an X-Health-Key header and returns 404 when no key is configured, so it's absent rather than locked on a deployment that hasn't enabled it.

GET /active-version sits at the root rather than under /api/v1 and is deliberately left out of the OpenAPI document. It's the endpoint to call when you need to know whether the fix you're waiting on is deployed.

Why this is worth doing

The usual failure with a tracking platform's API is that it covers reading and stops. You can pull yesterday's clicks, and everything that changes state happens in a browser. That holds until the month you onboard forty accounts, or repoint three hundred links after a rate renegotiation, or have to answer a client's question by joining your conversion data to spend data that lives somewhere else.

Writing a client against Routy means writing it against what the dashboard uses. /v1/composites/accounts/metrics exists because the sys-admin accounts table needed per-account numbers next to each row with server-side sorting on the metric columns, and the UI had been faking it with two report calls and a join in the browser. That endpoint shipped for the screen and is yours to call.

The parts worth checking before you commit are the ones with a cost. A token reaches everything the account can reach, so key handling is on you. Event exports stop at 120 days, so a warehouse backfill goes through the connector rather than the export endpoint. And there's no OAuth server, so a tool you build for other Routy customers asks each of them for a key.

Frequently asked questions

What's the base URL and the path format?

Paths sit under /api/v1 on the Routy host: https://platform.routy.app/api/v1/accounts. The version is in the path. Some reference pages write endpoints as /v1/accounts, which is the same route relative to the /api prefix.

Do I authenticate with a bearer token or an API key?

Both schemes work, and for anything running unattended you want the API key, since it isn't tied to a user session or a password. The header is Authorization: ApiKey <token>, not Bearer. A bearer JWT comes from POST /v1/auth and is what the dashboard uses. A few endpoints tied to a person, such as the current user's own profile, return 403 for a key and need a logged-in session instead.

Can I scope a key to read-only?

You can record reports.read on it and GET /v1/api-tokens/scopes tells you which scopes you're allowed to grant, but scopes don't yet restrict what a key can reach. Until they do, assume any key is account-wide, issue one per integration, and revoke rather than share.

What happens when a key expires?

Calls with it fail, and you get told well before that. A job each morning raises an issue 7 days out, escalates it in the final day, and flags it once the key is dead. Expiry defaults to a year and can't be set more than a year ahead, so a key has to be rotated on a schedule rather than left forever.

Can I generate an SDK?

Yes. The OpenAPI document for the client surface is at /api/swagger/v1/swagger.json and works with openapi-generator and the rest of that toolchain. Regenerating after a release picks up new endpoints. Sys-admin, frontend and legacy endpoints are published as separate documents, so the client document stays to what you're meant to call.

Is there a published rate limit?

There's no rate-limit table we'd ask you to build against. What you can see is your own usage: /v1/traffic-controls/api-usage/stats counts calls, 429 rejections and unique IPs over a rolling 72 hours, and the breakdown endpoint tells you which endpoint or which key the traffic came from. Treat a 429 as the instruction to back off, and talk to your account manager before a batch job you expect to be heavy.

Can I build a tool that connects to other people's Routy accounts?

Not through an authorisation flow, because Routy doesn't run an OAuth authorisation server. Routy is an OAuth client to Google Ads, Meta Ads and the other traffic sources it connects to, which is a different thing. Each customer of yours issues an API key from their own account and gives it to your tool.

Does the API push events to me, or do I poll?

Both exist for different things. Operational webhooks, registered at POST /v1/notifications/webhooks, send a signed JSON message when something on your account changes state that your systems should react to, with HMAC-SHA256 in a Routy-Webhook-Signature header, five retries over roughly seven hours, and delivery history. Conversion postbacks are a separate mechanism configured under your traffic sources. Report data is pull-only.

How do I tell which build is running?

GET /active-version returns the running version, build number, build time and commit sha.

Ready to try API?

Open your account settings, go to API Tokens and generate one, then copy the secret before you close the dialog. Call GET /api/v1/api-tokens with Authorization: ApiKey <your-token> to confirm it works, and read the reference at /api/docs for the endpoint you need next.