Getting Started
Routo's API lets you create and manage deep links programmatically. All endpoints live under a single base URL and accept JSON.
Base URL
All API requests use this base URL. Append the endpoint path to construct the full request URL.
https://api.routo.io Quick start
Create your first link with a single POST request. You'll need an API key — grab one from your developer settings.
curl https://api.routo.io/v1/links \
-H "X-API-Key: rto_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Product Launch",
"url": "https://shop.co/sneakers",
"projectId": "proj_xxxx",
"path": "sneakers"
}' Authentication
Every request needs an API key. Create keys in your developer settings — each key is scoped explicitly and bound to one or more projects.
X-API-Key header
Preferred. Pass the key in the X-API-Key header. The official SDK uses this format.
Keys are prefixed rto_ followed by 64 hex characters (68 chars total). The console shows the first 8 characters as a stable display prefix; the full key is visible only at creation time.
X-API-Key: rto_YOUR_API_KEY Bearer token (compatible)
Routo also accepts the standard Authorization: Bearer format for compatibility with existing tooling.
Authorization: Bearer rto_YOUR_API_KEY Scopes
Every key carries an explicit set of scopes. Calls outside those scopes return 403.
List links, list variants.
Create / update / delete links and variants.
All /v1/analytics/* + console summary.
Read custom domains.
Create / update / delete / verify custom domains.
Read project apps (iOS + Android registrations).
Create / update / delete project apps.
Read projects + organization metadata.
{
"name": "Production server",
"projectId": "proj_xxxx",
"scopes": ["links:write", "analytics:read"],
"projectIds": ["proj_xxxx"]
} Key creation is console-only — manage keys here.
Project allowlist
Every key is bound to a list of project IDs. The project context for each request is extracted from the route (body, query, path, or resolved from the resource being read) and matched against the allowlist. Mismatch returns 403.
Key may only act on those two projects.
Key may act on any project in its organization.
POST /v1/links from body.projectId
GET /v1/links from query.projectId
PUT /v1/links/:id resolved from resource
GET /v1/project-apps implicit (filtered to allowlist) Client-side safety
API keys grant project access defined by their scopes. Never bundle a rto_ key into a mobile app, browser bundle, or any code shipped to end users — it can be extracted from the binary.
For React Native / web clients, proxy link creation through your own backend or a serverless function that holds the key.
# Mobile app → your API → Routo
[RN client] --POST /create-link--> [your server]
|
+--X-API-Key: rto_YOUR_API_KEY--> api.routo.io Routes API keys cannot reach
The following operations are console-only and return 403 regardless of scope:
- User auth (
/v1/auth/*except/v1/auth/me) — keys are not user sessions. - Project create / update / delete.
- Organization, member, billing management.
- API key management itself (
/v1/developer/api-keys/*). - Preflight checks and the contact form.
Verify a key is still valid via GET /v1/auth/me — the only auth endpoint API keys may call (introspection).
TypeScript SDK
The official @codasignal/routo package is a zero-dependency, universal JavaScript SDK. It runs anywhere fetch and AbortSignal.any are available — Node ≥20, Bun, Deno, browsers, React Native, Cloudflare Workers, Vercel Edge.
Install
Install via your package manager of choice. The SDK ships ESM + CJS builds and full TypeScript types.
npm install @codasignal/routo
bun add @codasignal/routo
pnpm add @codasignal/routo Initialize
Construct a Routo client with your API key. In Node, call new Routo() with no arguments to read ROUTO_API_KEY from process.env.
Optional config: baseUrl, apiVersion, timeoutMs, maxRetries, retryBackoffMs, defaultHeaders, custom fetch.
import { Routo } from "@codasignal/routo";
const routo = new Routo(process.env.ROUTO_API_KEY!);
const link = await routo.links.create({
url: "https://example.com/products/42",
projectId: "proj_abc",
path: "summer-sale",
socialMedia: { title: "Summer Sale" },
}); Resources
The client exposes typed resource namespaces. Each method maps directly to a REST endpoint — see the API reference below for parameter details.
create, createDynalinks, list (returns Page<Link>), update, delete.
create, list, update, delete. A/B testing variants under a parent link.
get, aggregate, aggregateUnique, breakdown, variantPerformance.
get — dashboard summary + 7-day chart.
create, list (Page), get, update, delete, verify.
create, list (Page), listByProject, update, delete. iOS / Android registrations.
list, get.
list, current.
// AsyncIterable — iterate every link in the project
for await (const link of routo.links.list({ projectId: "p1" })) {
process(link);
}
// Manual paging
let page = routo.links.list({ projectId: "p1", limit: 50 });
let current = await page.next();
while (current) {
process(current.data);
current = current.hasMore ? await current.next() : null;
} // Per-call timeout
await routo.analytics.get(range, { timeoutMs: 5000 });
// AbortSignal
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000);
await routo.analytics.aggregate(range, { signal: ctrl.signal });
// Retries: 408 / 429 / 5xx auto-retried with exponential
// backoff up to maxRetries (default 2). POST/PATCH get an
// auto-generated Idempotency-Key reused across retries.
await routo.links.create(params, {
idempotencyKey: "create-link-42",
}); import {
RoutoValidationError,
RoutoRateLimitError,
RoutoAuthError,
RoutoNotFoundError,
RoutoServerError,
} from "@codasignal/routo";
try {
await routo.links.create(params);
} catch (err) {
if (err instanceof RoutoValidationError) {
for (const fe of err.fieldErrors) {
console.error(`${fe.path}: ${fe.message}`);
}
} else if (err instanceof RoutoRateLimitError) {
// honor err.retryAfterMs
} else throw err;
} Error classes
Every failure throws a typed subclass of RoutoError. requestId is attached when the server returns one.
Field-level validation failures. Exposes fieldErrors[].
code is unauthorized or forbidden.
Resource missing or out of scope.
Exposes retryAfterMs for backoff.
Retried automatically up to maxRetries.
Client-side: timeout, caller abort, transport failure. RoutoNetworkError.cause wraps the underlying error.
// Pure fetch — runs unmodified in:
// · Node ≥20 · Bun · Deno
// · Cloudflare Workers
// · Vercel Edge Functions
// · React Native (Hermes ≥0.74)
// No Node-specific imports.
// Single ESM + CJS build, sideEffects: false. CLI
@codasignal/routo-cli is a small command-line tool for wiring deep linking into React Native apps. It writes the iOS / Android config for you and verifies it against the live AASA / assetlinks responses. It does not call the Routo API and does not require an API key.
Install
Use as a one-shot via npx or install globally.
npx @codasignal/routo-cli --help
# or globally
npm install -g @codasignal/routo-cli routo setup
Configure deep linking for your React Native app. Detects Expo (managed or dynamic) or bare RN and writes the right config in place:
Edits app.json or app.config.ts.
Edits iOS .entitlements + Android AndroidManifest.xml intent filter.
Your Routo domain (e.g., go.shop.co). Prompts interactively if omitted.
npx @codasignal/routo-cli setup
# with explicit domain
npx @codasignal/routo-cli setup --domain go.shop.co routo doctor
Verify deep linking is wired correctly. Checks local config and fetches the live apple-app-site-association and assetlinks.json from your domain.
npx @codasignal/routo-cli doctor
npx @codasignal/routo-cli doctor --domain go.shop.co React Native
Routo has no native module — the universal SDK works in React Native out of the box because RN ships with fetch. Pair it with React Native's built-in Linking API. For initial setup, run npx @codasignal/routo-cli setup — it writes the iOS / Android config for you.
Install
Add the SDK to your app. No pods, no Gradle changes, no native rebuild — pure JavaScript.
Works with Expo (managed and bare) and bare React Native.
npm install @codasignal/routo
# or with Expo
npx expo install @codasignal/routo Create links from your backend
Don't ship rto_ keys in your app bundle. Instead, expose a small endpoint on your own server that holds the key and proxies create requests.
Your RN app calls your endpoint; your endpoint calls Routo with the SDK.
import Routo from "@codasignal/routo";
const routo = new Routo(process.env.ROUTO_API_KEY);
// POST /api/links
export async function handler(req) {
const link = await routo.links.create({
url: req.body.url,
projectId: "proj_xxxx",
path: req.body.path,
});
return Response.json(link);
} Receive links in the app
Configure Universal Links (iOS) and App Links (Android) by registering your iOS and Android apps in the Routo console. Routo serves the required AASA / assetlinks files automatically for every link tied to that app.
In React Native, listen to incoming URLs with the Linking API.
import { useEffect } from "react";
import { Linking } from "react-native";
function App() {
useEffect(() => {
// Cold start
Linking.getInitialURL().then((url) => {
if (url) handleLink(url);
});
// Warm app
const sub = Linking.addEventListener(
"url",
({ url }) => handleLink(url),
);
return () => sub.remove();
}, []);
return null;
} Resolve a link path
When your app receives a Routo URL, the path segment (e.g. /sneakers) identifies the link. Look it up via the list endpoint with search to find the destination URL and metadata.
function handleLink(url: string) {
const path = new URL(url).pathname.slice(1);
// Ask your server (which calls Routo)
fetch(`/api/links/resolve?path=${path}`)
.then((r) => r.json())
.then((link) => navigate(link.url));
} Receiving Links
Routo links open your app directly via Universal Links (iOS) and App Links (Android). Routo serves the platform association files for every domain you connect — no manual hosting.
Setup
Register your iOS and Android apps in the project dashboard. Provide:
Routo serves apple-app-site-association at your custom domain.
Routo serves .well-known/assetlinks.json.
.onOpenURL { url in
handleIncoming(url)
} val data: Uri? = intent?.data
data?.let { handleIncoming(it) } Linking.addEventListener("url", ({ url }) =>
handleIncoming(url),
); Fallback URLs
When the app isn't installed (or the link is opened on desktop), Routo redirects to a fallback. Configure per-platform URLs and force-redirect behavior on each link via advancedSettings.
advancedSettings fields
URL opened on iOS when the app is not installed.
URL opened on Android when the app is not installed.
Skip the interstitial and redirect immediately.
Hold the deep link payload across an App Store install so the app receives it on first launch.
Hold the deep link payload across a Play Store install so the app receives it on first launch.
{
"url": "https://shop.co/sneakers",
"advancedSettings": {
"iosFallbackUrl": "https://apps.apple.com/app/id123",
"androidFallbackUrl": "https://play.google.com/store/apps/details?id=co.shop",
"forceRedirect": true,
"iosDeferrerDeepLink": true,
"androidDeferrerDeepLink": true
}
} Links
/v1/links links:write Project: from body.projectId Create a branded short link or deep link for a project
Create a branded short link. Provide a destination URL; Routo allocates a slug under your default domain unless you set one explicitly.
Body parameters
curl https://api.routo.io/v1/links \
-H "Authorization: Bearer rto_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/launch",
"slug": "spring-launch",
"title": "Spring launch landing page",
"socialMedia": {
"title": "Spring launch",
"description": "Our biggest release yet.",
"imageUrl": "https://cdn.example.com/og/spring.png"
}
}' {
"id": "lnk_01HXYZABCDEF",
"url": "https://example.com/launch",
"slug": "spring-launch",
"shortUrl": "https://r.routo.io/spring-launch",
"domain": "r.routo.io",
"title": "Spring launch landing page",
"tags": [],
"createdAt": "2026-05-11T14:02:11.000Z",
"updatedAt": "2026-05-11T14:02:11.000Z"
} /v1/links/create links:write Project: from body.projectId Create a new link (Dynalinks-compatible)
Legacy create endpoint kept for SDK compatibility. Behaves identically to POST /v1/links — prefer that route for new integrations.
Body parameters
Android fallback URL
Custom domain ID (required for Routo users)
iOS fallback URL
A descriptive name for the link
The desired path for the dynamic link. If not provided, a random one will be generated.
Project ID (required for Routo users)
Social description
Social image URL
Social title
The destination URL the dynamic link should point to
curl https://api.routo.io/v1/links/create \
-H "Authorization: Bearer rto_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/promo",
"slug": "promo-2026"
}' {
"id": "lnk_01HXYZCOMPAT1",
"url": "https://example.com/promo",
"slug": "promo-2026",
"shortUrl": "https://r.routo.io/promo-2026",
"domain": "r.routo.io",
"createdAt": "2026-05-11T14:05:42.000Z",
"updatedAt": "2026-05-11T14:05:42.000Z"
} /v1/links links:read Project: from query.projectId Get all links
List links in the current project. Supports cursor pagination and tag filtering via query params.
Body parameters
No body parameters
curl "https://api.routo.io/v1/links?limit=20&tag=campaign" \
-H "Authorization: Bearer rto_..." {
"data": [
{
"id": "lnk_01HXYZABCDEF",
"url": "https://example.com/launch",
"slug": "spring-launch",
"shortUrl": "https://r.routo.io/spring-launch",
"tags": ["campaign"],
"createdAt": "2026-05-11T14:02:11.000Z"
}
],
"nextCursor": null
} /v1/links/:id links:write Project: resolved from resource Update a link
Update an existing link by id. Only the fields you send are changed — omit a field to leave it untouched.
Body parameters
curl -X PUT https://api.routo.io/v1/links/lnk_01HXYZABCDEF \
-H "Authorization: Bearer rto_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/launch-v2",
"title": "Spring launch (refresh)"
}' {
"id": "lnk_01HXYZABCDEF",
"url": "https://example.com/launch-v2",
"slug": "spring-launch",
"shortUrl": "https://r.routo.io/spring-launch",
"title": "Spring launch (refresh)",
"updatedAt": "2026-05-11T15:11:08.000Z"
} /v1/links/:id links:write Project: resolved from resource Delete a link
Soft-delete a link. The slug becomes unresolvable; historical analytics remain queryable for the retention window.
Body parameters
curl -X DELETE https://api.routo.io/v1/links/lnk_01HXYZABCDEF \
-H "Authorization: Bearer rto_..." {
"id": "lnk_01HXYZABCDEF",
"deleted": true
} Link Variants
/v1/links/:linkId/variants links:write Project: resolved from resource Create an A/B testing variant for a link
Create a variant under an existing link. Variants split traffic by weight — the sum of all weights determines the distribution.
Body parameters
curl https://api.routo.io/v1/links/lnk_01HXYZABCDEF/variants \
-H "Authorization: Bearer rto_..." \
-H "Content-Type: application/json" \
-d '{
"name": "control",
"url": "https://example.com/launch",
"weight": 50
}' {
"id": "var_01HXYZVARIANT1",
"linkId": "lnk_01HXYZABCDEF",
"name": "control",
"url": "https://example.com/launch",
"weight": 50,
"createdAt": "2026-05-11T15:20:00.000Z"
} /v1/links/:linkId/variants links:read Project: resolved from resource List A/B testing variants for a link
List all variants attached to a link, in creation order. Use this to inspect current weight distribution before updating.
Body parameters
No body parameters
curl https://api.routo.io/v1/links/lnk_01HXYZABCDEF/variants \
-H "Authorization: Bearer rto_..." {
"data": [
{
"id": "var_01HXYZVARIANT1",
"linkId": "lnk_01HXYZABCDEF",
"name": "control",
"url": "https://example.com/launch",
"weight": 50
},
{
"id": "var_01HXYZVARIANT2",
"linkId": "lnk_01HXYZABCDEF",
"name": "treatment",
"url": "https://example.com/launch-v2",
"weight": 50
}
]
} /v1/links/:linkId/variants/:variantId links:write Project: resolved from resource Update an A/B testing variant
Update a variant's name, destination, or weight. Re-balancing weights takes effect on the next click — in-flight redirects are not affected.
Body parameters
curl -X PATCH https://api.routo.io/v1/links/lnk_01HXYZABCDEF/variants/var_01HXYZVARIANT2 \
-H "Authorization: Bearer rto_..." \
-H "Content-Type: application/json" \
-d '{
"weight": 70
}' {
"id": "var_01HXYZVARIANT2",
"linkId": "lnk_01HXYZABCDEF",
"name": "treatment",
"url": "https://example.com/launch-v2",
"weight": 70,
"updatedAt": "2026-05-11T16:00:00.000Z"
} /v1/links/:linkId/variants/:variantId links:write Project: resolved from resource Delete an A/B testing variant
Remove a variant from a link. Remaining variants retain their existing weights — the system does not auto-rebalance.
Body parameters
No body parameters
curl -X DELETE https://api.routo.io/v1/links/lnk_01HXYZABCDEF/variants/var_01HXYZVARIANT2 \
-H "Authorization: Bearer rto_..." {
"id": "var_01HXYZVARIANT2",
"deleted": true
} Custom Domains
/v1/custom-domains domains:write Project: from body.projectId Create a new custom domain
Register a new custom domain. The response includes the DNS record you must publish before calling verify.
Body parameters
curl https://api.routo.io/v1/custom-domains \
-H "Authorization: Bearer rto_..." \
-H "Content-Type: application/json" \
-d '{
"domain": "links.example.com"
}' {
"id": "dom_01HXYZDOMAIN2",
"domain": "links.example.com",
"status": "pending",
"verification": {
"type": "CNAME",
"name": "links.example.com",
"value": "cname.routo.io"
},
"createdAt": "2026-05-11T14:30:00.000Z"
} /v1/custom-domains/:id/verify domains:write Project: resolved from resource Verify if a custom domain or subdomain is properly configured and active
Trigger DNS verification for a pending domain. Returns the live status once Routo confirms the CNAME resolves.
Body parameters
No body parameters
curl -X POST https://api.routo.io/v1/custom-domains/dom_01HXYZDOMAIN2/verify \
-H "Authorization: Bearer rto_..." {
"id": "dom_01HXYZDOMAIN2",
"domain": "links.example.com",
"status": "verified",
"verifiedAt": "2026-05-11T14:35:21.000Z"
} /v1/custom-domains domains:read Project: from query.projectId Get all custom domains
List custom domains attached to the current project, including verification status and DNS targets.
Body parameters
No body parameters
curl https://api.routo.io/v1/custom-domains \
-H "Authorization: Bearer rto_..." {
"data": [
{
"id": "dom_01HXYZDOMAIN1",
"domain": "links.example.com",
"status": "verified",
"target": "cname.routo.io",
"createdAt": "2026-04-22T09:14:00.000Z"
}
]
} /v1/custom-domains/:id domains:read Project: resolved from resource Get a custom domain by ID
Fetch a single custom domain by id, including verification status and the DNS record needed to complete setup.
Body parameters
No body parameters
curl https://api.routo.io/v1/custom-domains/dom_01HXYZDOMAIN1 \
-H "Authorization: Bearer rto_..." {
"id": "dom_01HXYZDOMAIN1",
"domain": "links.example.com",
"status": "verified",
"target": "cname.routo.io",
"createdAt": "2026-04-22T09:14:00.000Z",
"verifiedAt": "2026-04-22T09:18:42.000Z"
} /v1/custom-domains/:id domains:write Project: resolved from resource Update a custom domain
Update mutable fields on a custom domain. Most fields are read-only — typically you only update the friendly name or routing target.
Body parameters
curl -X PUT https://api.routo.io/v1/custom-domains/dom_01HXYZDOMAIN1 \
-H "Authorization: Bearer rto_..." \
-H "Content-Type: application/json" \
-d '{
"routoDomain": "r2.routo.io"
}' {
"id": "dom_01HXYZDOMAIN1",
"domain": "links.example.com",
"routoDomain": "r2.routo.io",
"status": "verified",
"updatedAt": "2026-05-11T16:30:00.000Z"
} /v1/custom-domains/:id domains:write Project: resolved from resource Delete a custom domain
Detach a custom domain from the project. Active links served from this domain will stop resolving — repoint DNS before calling this.
Body parameters
curl -X DELETE https://api.routo.io/v1/custom-domains/dom_01HXYZDOMAIN1 \
-H "Authorization: Bearer rto_..." {
"id": "dom_01HXYZDOMAIN1",
"deleted": true
} Project Apps
/v1/project-apps apps:write Project: from body.projectId Create a project app
Register an iOS or Android app. Routo regenerates the AASA / assetlinks payloads automatically once the app is saved.
Body parameters
curl https://api.routo.io/v1/project-apps \
-H "Authorization: Bearer rto_..." \
-H "Content-Type: application/json" \
-d '{
"platform": "ios",
"bundleId": "com.example.app",
"teamId": "ABCDE12345"
}' {
"id": "app_01HXYZIOS2",
"platform": "ios",
"bundleId": "com.example.app",
"teamId": "ABCDE12345",
"createdAt": "2026-05-11T14:45:00.000Z"
} /v1/project-apps apps:read Project: implicit (filtered to allowlist) Get all project apps
List the iOS and Android apps registered for the current project. Routo uses these to serve Apple AASA and Android assetlinks files.
Body parameters
No body parameters
curl https://api.routo.io/v1/project-apps \
-H "Authorization: Bearer rto_..." {
"data": [
{
"id": "app_01HXYZIOS1",
"platform": "ios",
"bundleId": "com.example.app",
"teamId": "ABCDE12345",
"createdAt": "2026-04-01T10:00:00.000Z"
},
{
"id": "app_01HXYZAND1",
"platform": "android",
"packageName": "com.example.app",
"sha256CertFingerprints": ["AA:BB:CC:..."],
"createdAt": "2026-04-01T10:01:00.000Z"
}
]
} /v1/project-apps/:projectId apps:read Project: from path.projectId Get all project apps by project ID
List the apps registered for a specific project. Useful when an API key spans multiple projects and you need to scope by project id.
Body parameters
No body parameters
curl https://api.routo.io/v1/project-apps/prj_01HXYZPROJECT1 \
-H "Authorization: Bearer rto_..." {
"data": [
{
"id": "app_01HXYZIOS1",
"projectId": "prj_01HXYZPROJECT1",
"platform": "ios",
"identifier": "com.example.app"
}
]
} /v1/project-apps/:id apps:write Project: resolved from resource Update a project app by ID
Update a registered app. Changing identifier or platform regenerates the AASA / assetlinks files served from the linked custom domain.
Body parameters
curl -X PUT https://api.routo.io/v1/project-apps/app_01HXYZIOS1 \
-H "Authorization: Bearer rto_..." \
-H "Content-Type: application/json" \
-d '{
"id": "app_01HXYZIOS1",
"identifier": "com.example.app2",
"settings": { "appStoreId": "1234567890" }
}' {
"id": "app_01HXYZIOS1",
"platform": "ios",
"identifier": "com.example.app2",
"settings": { "appStoreId": "1234567890" },
"updatedAt": "2026-05-11T17:00:00.000Z"
} /v1/project-apps/:id apps:write Project: resolved from resource Delete a project app by ID
Unregister an app. AASA / assetlinks payloads are regenerated immediately — deep link handoffs to the removed app stop working on next fetch.
Body parameters
curl -X DELETE https://api.routo.io/v1/project-apps/app_01HXYZIOS1 \
-H "Authorization: Bearer rto_..." {
"id": "app_01HXYZIOS1",
"deleted": true
} Analytics
/v1/console analytics:read Project: from query.projectId Get console summary and 7-day chart
Console summary plus a seven-day click chart for the current project. Powers the dashboard home view.
Body parameters
No body parameters
curl https://api.routo.io/v1/console \
-H "Authorization: Bearer rto_..." {
"totals": {
"links": 42,
"clicksLast7d": 18234,
"uniqueVisitorsLast7d": 9120
},
"chart": [
{ "date": "2026-05-05", "clicks": 2410 },
{ "date": "2026-05-06", "clicks": 2588 },
{ "date": "2026-05-07", "clicks": 2701 },
{ "date": "2026-05-08", "clicks": 2855 },
{ "date": "2026-05-09", "clicks": 2620 },
{ "date": "2026-05-10", "clicks": 2730 },
{ "date": "2026-05-11", "clicks": 2330 }
]
} /v1/analytics analytics:read Project: from query.projectId Get analytics for a project or link over a time range
Stream raw click events for the current project. Filter by link id, date range, or country; paginate with the returned cursor.
Body parameters
No body parameters
curl "https://api.routo.io/v1/analytics?linkId=lnk_01HXYZABCDEF&from=2026-05-01&to=2026-05-11" \
-H "Authorization: Bearer rto_..." {
"data": [
{
"id": "evt_01HXYZCLICK1",
"linkId": "lnk_01HXYZABCDEF",
"timestamp": "2026-05-10T18:22:04.000Z",
"country": "US",
"device": "mobile",
"os": "iOS",
"browser": "Safari",
"referrer": "https://twitter.com"
}
],
"nextCursor": "evt_01HXYZCLICK1"
} /v1/analytics/aggregate analytics:read Project: from query.projectId Get counts aggregated by day
Aggregate click counts over a time range. Group by day or hour with the bucket query param.
Body parameters
No body parameters
curl "https://api.routo.io/v1/analytics/aggregate?from=2026-05-01&to=2026-05-11&bucket=day" \
-H "Authorization: Bearer rto_..." {
"bucket": "day",
"data": [
{ "ts": "2026-05-01", "clicks": 1840 },
{ "ts": "2026-05-02", "clicks": 1922 },
{ "ts": "2026-05-03", "clicks": 2010 }
]
} /v1/analytics/breakdown analytics:read Project: from query.projectId Get device distribution, top countries, and top referrers
Click counts grouped by a dimension — country, device, OS, browser, or referrer. Use this to power top-N tables.
Body parameters
No body parameters
curl "https://api.routo.io/v1/analytics/breakdown?dimension=country&from=2026-05-01&to=2026-05-11" \
-H "Authorization: Bearer rto_..." {
"dimension": "country",
"data": [
{ "key": "US", "clicks": 8421 },
{ "key": "BR", "clicks": 3210 },
{ "key": "DE", "clicks": 2104 },
{ "key": "JP", "clicks": 1809 }
]
} /v1/analytics/aggregate-unique analytics:read Project: from query.projectId Get unique visitors aggregated by day
Same shape as aggregate, but counts unique visitors instead of raw clicks. Useful for de-duplicated reach numbers.
Body parameters
No body parameters
curl "https://api.routo.io/v1/analytics/aggregate-unique?from=2026-05-01&to=2026-05-11&bucket=day" \
-H "Authorization: Bearer rto_..." {
"bucket": "day",
"data": [
{ "ts": "2026-05-01", "uniqueVisitors": 940 },
{ "ts": "2026-05-02", "uniqueVisitors": 1011 },
{ "ts": "2026-05-03", "uniqueVisitors": 1088 }
]
} /v1/analytics/variant-performance analytics:read Project: from query.projectId Get click counts per variant for a link
Click and conversion rate per variant for a given link. Use this to decide which arm of an A/B test to ship.
Body parameters
No body parameters
curl "https://api.routo.io/v1/analytics/variant-performance?linkId=lnk_01HXYZABCDEF&from=2026-05-01&to=2026-05-11" \
-H "Authorization: Bearer rto_..." {
"linkId": "lnk_01HXYZABCDEF",
"data": [
{
"variantId": "var_01HXYZVARIANT1",
"name": "control",
"clicks": 4120,
"conversions": 312,
"conversionRate": 0.0757
},
{
"variantId": "var_01HXYZVARIANT2",
"name": "treatment",
"clicks": 4081,
"conversions": 388,
"conversionRate": 0.0951
}
]
} Projects
/v1/projects projects:read Project: implicit (filtered to allowlist) Get all projects
List projects accessible to the current API key. Most keys are scoped to a single project; org-scoped keys see all projects in the org.
Body parameters
No body parameters
curl https://api.routo.io/v1/projects \
-H "Authorization: Bearer rto_..." {
"data": [
{
"id": "prj_01HXYZPROJECT1",
"name": "Marketing site",
"slug": "marketing-site",
"organizationId": "org_01HXYZORG1",
"createdAt": "2026-03-12T08:00:00.000Z"
}
]
} /v1/projects/:id projects:read Project: from path.id Get a project by ID
Fetch a single project by id, including its slug, organization, and creation timestamp.
Body parameters
No body parameters
curl https://api.routo.io/v1/projects/prj_01HXYZPROJECT1 \
-H "Authorization: Bearer rto_..." {
"id": "prj_01HXYZPROJECT1",
"name": "Marketing site",
"slug": "marketing-site",
"organizationId": "org_01HXYZORG1",
"createdAt": "2026-03-12T08:00:00.000Z"
} /v1/organizations projects:read Project: org-wide Get all organizations the user belongs to, including membership info
List organizations the current key has access to. Useful when building org switchers in dashboards.
Body parameters
No body parameters
curl https://api.routo.io/v1/organizations \
-H "Authorization: Bearer rto_..." {
"data": [
{
"id": "org_01HXYZORG1",
"name": "Example, Inc.",
"slug": "example",
"createdAt": "2026-02-01T10:00:00.000Z"
}
]
} /v1/organizations/current projects:read Project: org-wide Get current organization with team members and user's membership info
Return the organization that owns the current API key. Equivalent to looking up the key's project then resolving its org.
Body parameters
No body parameters
curl https://api.routo.io/v1/organizations/current \
-H "Authorization: Bearer rto_..." {
"id": "org_01HXYZORG1",
"name": "Example, Inc.",
"slug": "example",
"plan": "pro"
} Introspection
/v1/auth/me Get current user
Verify the current API key. Returns the key's scopes, project, and a stable identifier — useful as a liveness probe.
Body parameters
No body parameters
curl https://api.routo.io/v1/auth/me \
-H "Authorization: Bearer rto_..." {
"keyId": "key_01HXYZKEY1",
"projectId": "prj_01HXYZPROJECT1",
"organizationId": "org_01HXYZORG1",
"scopes": ["links:read", "links:write", "analytics:read"]
} Errors
Routo uses standard HTTP status codes. Validation and business-logic failures return a structured body; unhandled server errors may omit the cause field.
Request succeeded.
Invalid or missing parameters. Includes cause.field and cause.hint when the source is identifiable.
Missing or invalid API key.
Key's scope or project allowlist doesn't permit the request.
Resource doesn't exist.
Too many requests. Back off and retry.
Something broke on our end. May omit cause.
{
"message": "No custom domains found for this project.",
"code": "BAD_REQUEST",
"cause": {
"field": "customDomainId",
"hint": "Create a domain first or specify customDomainId"
}
} {
"message": "Internal Server Error",
"code": "INTERNAL_SERVER_ERROR"
}