Skip to main content

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.

Base 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.

terminal
cURL
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.

Header
X-API-Key: rto_YOUR_API_KEY

Bearer token (compatible)

Routo also accepts the standard Authorization: Bearer format for compatibility with existing tooling.

Header
Authorization: Bearer rto_YOUR_API_KEY

Scopes

Every key carries an explicit set of scopes. Calls outside those scopes return 403.

links:read scope

List links, list variants.

links:write scope

Create / update / delete links and variants.

analytics:read scope

All /v1/analytics/* + console summary.

domains:read scope

Read custom domains.

domains:write scope

Create / update / delete / verify custom domains.

apps:read scope

Read project apps (iOS + Android registrations).

apps:write scope

Create / update / delete project apps.

projects:read scope

Read projects + organization metadata.

key creation payload JSON
{
  "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.

projectIds: ["proj_a","proj_b"] array

Key may only act on those two projects.

projectIds: ["*"] wildcard

Key may act on any project in its organization.

Project context per route
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.

Recommended flow
# 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.

terminal npm / bun / pnpm
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.

TypeScript
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.

routo.links → LinksResource

create, createDynalinks, list (returns Page<Link>), update, delete.

routo.links.variants → LinkVariantsResource

create, list, update, delete. A/B testing variants under a parent link.

routo.analytics → AnalyticsResource

get, aggregate, aggregateUnique, breakdown, variantPerformance.

routo.console → ConsoleResource

get — dashboard summary + 7-day chart.

routo.customDomains → CustomDomainsResource

create, list (Page), get, update, delete, verify.

routo.projectApps → ProjectAppsResource

create, list (Page), listByProject, update, delete. iOS / Android registrations.

routo.projects → ProjectsResource

list, get.

routo.organizations → OrganizationsResource

list, current.

pagination — Page<T>
// 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;
}
timeout, abort, retries
// 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",
});
typed errors
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.

RoutoValidationError 400

Field-level validation failures. Exposes fieldErrors[].

RoutoAuthError 401 / 403

code is unauthorized or forbidden.

RoutoNotFoundError 404

Resource missing or out of scope.

RoutoRateLimitError 429

Exposes retryAfterMs for backoff.

RoutoServerError 5xx

Retried automatically up to maxRetries.

RoutoTimeoutError / RoutoAbortError / RoutoNetworkError

Client-side: timeout, caller abort, transport failure. RoutoNetworkError.cause wraps the underlying error.

runtime support
// 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.

terminal
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:

Expo

Edits app.json or app.config.ts.

Bare React Native

Edits iOS .entitlements + Android AndroidManifest.xml intent filter.

--domain <host> flag

Your Routo domain (e.g., go.shop.co). Prompts interactively if omitted.

terminal
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.

terminal
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.

terminal
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.

your-server.ts
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.

App.tsx
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.

resolve.ts
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));
}

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

iosFallbackUrl string

URL opened on iOS when the app is not installed.

androidFallbackUrl string

URL opened on Android when the app is not installed.

forceRedirect boolean

Skip the interstitial and redirect immediately.

iosDeferrerDeepLink boolean

Hold the deep link payload across an App Store install so the app receives it on first launch.

androidDeferrerDeepLink boolean

Hold the deep link payload across a Play Store install so the app receives it on first launch.

advancedSettings JSON
{
  "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
  }
}

Custom Domains

POST /v1/custom-domains
Auth required Scope: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

customDomain string
customHostnameId unknown
domain unknown
organizationId uuid
projectId uuid required
routoDomain string required
setupMethod unknown
subdomainWorkerId unknown
validationHttpBody unknown
validationHttpUrl unknown
validationTxtName unknown
validationTxtValue unknown
Request
JSON
curl https://api.routo.io/v1/custom-domains \
  -H "Authorization: Bearer rto_..." \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "links.example.com"
  }'
Response
200
{
  "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"
}
POST /v1/custom-domains/:id/verify
Auth required Scope: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

Request
JSON
curl -X POST https://api.routo.io/v1/custom-domains/dom_01HXYZDOMAIN2/verify \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "id": "dom_01HXYZDOMAIN2",
  "domain": "links.example.com",
  "status": "verified",
  "verifiedAt": "2026-05-11T14:35:21.000Z"
}
GET /v1/custom-domains
Auth required Scope: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

Request
JSON
curl https://api.routo.io/v1/custom-domains \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "data": [
    {
      "id": "dom_01HXYZDOMAIN1",
      "domain": "links.example.com",
      "status": "verified",
      "target": "cname.routo.io",
      "createdAt": "2026-04-22T09:14:00.000Z"
    }
  ]
}
GET /v1/custom-domains/:id
Auth required Scope: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

Request
JSON
curl https://api.routo.io/v1/custom-domains/dom_01HXYZDOMAIN1 \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "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"
}
PUT /v1/custom-domains/:id
Auth required Scope: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

createdAt unknown
createdBy unknown
customDomain string
customHostnameId unknown
domain unknown
id uuid required
lastVerifiedAt unknown
organizationId uuid
projectId uuid
routoDomain string
setupMethod unknown
subdomainWorkerId unknown
updatedAt unknown
validationHttpBody unknown
validationHttpUrl unknown
validationTxtName unknown
validationTxtValue unknown
verificationStatus "pending" | "active" | "failed"
Request
JSON
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"
  }'
Response
200
{
  "id": "dom_01HXYZDOMAIN1",
  "domain": "links.example.com",
  "routoDomain": "r2.routo.io",
  "status": "verified",
  "updatedAt": "2026-05-11T16:30:00.000Z"
}
DELETE /v1/custom-domains/:id
Auth required Scope: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

id string required
Request
JSON
curl -X DELETE https://api.routo.io/v1/custom-domains/dom_01HXYZDOMAIN1 \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "id": "dom_01HXYZDOMAIN1",
  "deleted": true
}

Project Apps

POST /v1/project-apps
Auth required Scope: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

customDomainId string required
identifier string required
platform "ios" | "android" required
projectId string required
settings unknown required
Request
JSON
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"
  }'
Response
200
{
  "id": "app_01HXYZIOS2",
  "platform": "ios",
  "bundleId": "com.example.app",
  "teamId": "ABCDE12345",
  "createdAt": "2026-05-11T14:45:00.000Z"
}
GET /v1/project-apps
Auth required Scope: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

Request
JSON
curl https://api.routo.io/v1/project-apps \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "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"
    }
  ]
}
GET /v1/project-apps/:projectId
Auth required Scope: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

Request
JSON
curl https://api.routo.io/v1/project-apps/prj_01HXYZPROJECT1 \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "data": [
    {
      "id": "app_01HXYZIOS1",
      "projectId": "prj_01HXYZPROJECT1",
      "platform": "ios",
      "identifier": "com.example.app"
    }
  ]
}
PUT /v1/project-apps/:id
Auth required Scope: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

customDomainId string
id string required
identifier string
platform "ios" | "android"
projectId string
settings unknown
Request
JSON
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" }
  }'
Response
200
{
  "id": "app_01HXYZIOS1",
  "platform": "ios",
  "identifier": "com.example.app2",
  "settings": { "appStoreId": "1234567890" },
  "updatedAt": "2026-05-11T17:00:00.000Z"
}
DELETE /v1/project-apps/:id
Auth required Scope: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

id string required
Request
JSON
curl -X DELETE https://api.routo.io/v1/project-apps/app_01HXYZIOS1 \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "id": "app_01HXYZIOS1",
  "deleted": true
}

Analytics

GET /v1/console
Auth required Scope: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

Request
JSON
curl https://api.routo.io/v1/console \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "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 }
  ]
}
GET /v1/analytics
Auth required Scope: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

Request
JSON
curl "https://api.routo.io/v1/analytics?linkId=lnk_01HXYZABCDEF&amp;from=2026-05-01&amp;to=2026-05-11" \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "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"
}
GET /v1/analytics/aggregate
Auth required Scope: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

Request
JSON
curl "https://api.routo.io/v1/analytics/aggregate?from=2026-05-01&amp;to=2026-05-11&amp;bucket=day" \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "bucket": "day",
  "data": [
    { "ts": "2026-05-01", "clicks": 1840 },
    { "ts": "2026-05-02", "clicks": 1922 },
    { "ts": "2026-05-03", "clicks": 2010 }
  ]
}
GET /v1/analytics/breakdown
Auth required Scope: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

Request
JSON
curl "https://api.routo.io/v1/analytics/breakdown?dimension=country&amp;from=2026-05-01&amp;to=2026-05-11" \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "dimension": "country",
  "data": [
    { "key": "US", "clicks": 8421 },
    { "key": "BR", "clicks": 3210 },
    { "key": "DE", "clicks": 2104 },
    { "key": "JP", "clicks": 1809 }
  ]
}
GET /v1/analytics/aggregate-unique
Auth required Scope: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

Request
JSON
curl "https://api.routo.io/v1/analytics/aggregate-unique?from=2026-05-01&amp;to=2026-05-11&amp;bucket=day" \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "bucket": "day",
  "data": [
    { "ts": "2026-05-01", "uniqueVisitors": 940 },
    { "ts": "2026-05-02", "uniqueVisitors": 1011 },
    { "ts": "2026-05-03", "uniqueVisitors": 1088 }
  ]
}
GET /v1/analytics/variant-performance
Auth required Scope: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

Request
JSON
curl "https://api.routo.io/v1/analytics/variant-performance?linkId=lnk_01HXYZABCDEF&amp;from=2026-05-01&amp;to=2026-05-11" \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "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

GET /v1/projects
Auth required Scope: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

Request
JSON
curl https://api.routo.io/v1/projects \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "data": [
    {
      "id": "prj_01HXYZPROJECT1",
      "name": "Marketing site",
      "slug": "marketing-site",
      "organizationId": "org_01HXYZORG1",
      "createdAt": "2026-03-12T08:00:00.000Z"
    }
  ]
}
GET /v1/projects/:id
Auth required Scope: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

Request
JSON
curl https://api.routo.io/v1/projects/prj_01HXYZPROJECT1 \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "id": "prj_01HXYZPROJECT1",
  "name": "Marketing site",
  "slug": "marketing-site",
  "organizationId": "org_01HXYZORG1",
  "createdAt": "2026-03-12T08:00:00.000Z"
}
GET /v1/organizations
Auth required Scope: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

Request
JSON
curl https://api.routo.io/v1/organizations \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "data": [
    {
      "id": "org_01HXYZORG1",
      "name": "Example, Inc.",
      "slug": "example",
      "createdAt": "2026-02-01T10:00:00.000Z"
    }
  ]
}
GET /v1/organizations/current
Auth required Scope: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

Request
JSON
curl https://api.routo.io/v1/organizations/current \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "id": "org_01HXYZORG1",
  "name": "Example, Inc.",
  "slug": "example",
  "plan": "pro"
}

Introspection

GET /v1/auth/me
Auth required Introspect Project: org-wide

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

Request
JSON
curl https://api.routo.io/v1/auth/me \
  -H "Authorization: Bearer rto_..."
Response
200
{
  "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.

200 OK

Request succeeded.

400 Bad Request

Invalid or missing parameters. Includes cause.field and cause.hint when the source is identifiable.

401 Unauthorized

Missing or invalid API key.

403 Forbidden

Key's scope or project allowlist doesn't permit the request.

404 Not Found

Resource doesn't exist.

429 Rate Limited

Too many requests. Back off and retry.

500 Server Error

Something broke on our end. May omit cause.

Structured 400 error
400
{
  "message": "No custom domains found for this project.",
  "code": "BAD_REQUEST",
  "cause": {
    "field": "customDomainId",
    "hint": "Create a domain first or specify customDomainId"
  }
}
Unhandled 500 error
500
{
  "message": "Internal Server Error",
  "code": "INTERNAL_SERVER_ERROR"
}