API integration
Routix provides a versioned REST API that allows external applications, such as ERP systems, WMS platforms, BI tools, and AI agents, to securely access your transport management data.
The API is built on OAuth 2.1 and uses a proxy layer to provide stability, versioning, and fine-grained access control.
User level: Advanced
OAuth flow
Routix uses OAuth 2.1 to authenticate external applications. Two flows are available depending on your integration type.
Flow 1: Authorization Code + PKCE
Use this flow when your application needs to act on behalf of a Routix user. The user explicitly grants consent and selects the organization that the application may access.
Sequence
External App Routix Auth User
| | |
|-- redirect to /authorize --> | |
| (client_id, scope, | |
| code_challenge, state) | |
| |-- consent screen -----> |
| | (app name, scopes, |
| | org selector) |
| | <-- approve + org ----- |
| <-- redirect with ?code= --- | |
| | |
|-- POST /oauth/token -------> | |
| (code, code_verifier) | |
| <-- { access_token } ------- | |
| | |
|-- GET /api/v1/{branch}/...-> | (API Proxy) |
| <-- { data: [...] } -------- | |Step 1: Generate PKCE challenge
Before starting the flow, generate a PKCE code verifier and challenge:
// Generate code_verifier (43-128 characters)
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const codeVerifier = btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
// Generate code_challenge (SHA-256, base64url)
const digest = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(codeVerifier)
);
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');Step 2: Redirect to authorize
Redirect the user’s browser to:
https://api.routix.com/auth/v1/oauth/authorize
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&response_type=code
&code_challenge=BASE64URL_SHA256_HASH
&code_challenge_method=S256
&scope=routix:accounts:read routix:orders:read
&state=RANDOM_STATE_VALUE| Parameter | Required | Description |
|---|---|---|
| client_id | Yes | Your OAuth app client ID (UUID). |
| redirect_uri | Yes | Must exactly match one of your registered redirect URIs. |
| response_type | Yes | Always code. |
| code_challenge | Yes | Base64url-encoded SHA-256 hash of your code_verifier. |
| code_challenge_method | Yes | Always S256. |
| scope | Yes | Space-separated list of requested scopes. |
| state | Recommended | Random string to prevent CSRF attacks. |
Step 3: User consent
Routix shows a consent screen where the user:
- Sees the application name and all requested scopes.
- Selects which organization to grant access to.
- Approves or denies the request.
On approval:
- A record is created in
oauth_client_authorizationsthat links the app to the selected organization. - The user is redirected back to your
redirect_uriwith?code=xxx&state=xxx.
Step 4: Exchange code for tokens
curl -X POST https://api.routix.com/auth/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "AUTHORIZATION_CODE",
"redirect_uri": "https://yourapp.com/callback",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "routix_a1b2c3d4...",
"code_verifier": "YOUR_ORIGINAL_CODE_VERIFIER"
}'Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "v1.MjQ0ZWY5..."
}Step 5: Refresh tokens
Access tokens expire after 1 hour. Use the refresh token to request a new one:
curl -X POST https://api.routix.com/auth/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "v1.MjQ0ZWY5..."
}'Refresh tokens are single-use and rotate on every refresh.
Token claims: Authorization Code
The JWT is enriched by a custom access token hook that adds:
{
"sub": "user-uuid",
"aud": "authenticated",
"role": "authenticated",
"organization_id": "org-uuid",
"oauth_client_id": "client-uuid",
"oauth_scopes": ["routix:accounts:read", "routix:orders:read"]
}These claims are enforced by Row Level Security (RLS) in the database.
Flow 2: Client credentials
Use this flow for server-to-server communication. No user interaction is required.
Sequence
Your Server Routix
| |
|-- POST /oauth-token -------> |
| (client_id, client_secret) |
| <-- { access_token } ------- |
| |
|-- GET /api/v1/{branch}/...-> |
| <-- { data: [...] } -------- |Request a token
curl -X POST https://api.routix.com/functions/v1/oauth-token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "routix_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
}'Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "routix:accounts:read routix:orders:read",
"organization_id": "org-uuid",
"branch_ids": ["branch-uuid-1", "branch-uuid-2"]
}How it works
- Your server sends
client_idandclient_secretto the token endpoint. - Routix verifies the secret by comparing SHA-256 hashes.
- A JWT is built with the app’s
organization_id,allowed_scopes, andallowed_branch_ids. - The JWT is signed with HS256 and returned.
- You use the token to call the API proxy.
Token claims: Client credentials
{
"iss": "supabase",
"aud": "authenticated",
"role": "authenticated",
"sub": "app-uuid",
"organization_id": "org-uuid",
"oauth_client_id": "client-uuid",
"oauth_scopes": ["routix:accounts:read"],
"oauth_branch_ids": ["branch-uuid-1"],
"oauth_app_name": "My ERP Sync"
}Comparison
| Aspect | Authorization Code + PKCE | Client credentials |
|---|---|---|
| User interaction | Yes, consent screen | No |
| Token endpoint | /auth/v1/oauth/token | /functions/v1/oauth-token |
| Organization binding | Selected by user during consent | From app registration |
| Refresh tokens | Yes, rotating | No, request a new token |
| Use case | User-facing apps, marketplace | Backend sync, BI, automation |
Security best practices
| Practice | Why |
|---|---|
| Always use PKCE with S256 | Required for Authorization Code flow. Plain challenges are not supported. |
| Store secrets securely | The client secret is shown only once. Use a secrets manager and never store it in source code. |
Validate the state parameter | Prevents CSRF attacks during the redirect flow. |
| Request minimal scopes | Only request the permissions your application actually needs. |
| Rotate secrets regularly | Use secret rotation to limit exposure. |
| Handle token expiry | Tokens expire in 1 hour. Refresh or request a new token. |
| Never expose secrets in frontend code | Client credentials must only be used from a backend server. |
Architecture
The integration system consists of three layers:
| Layer | Description |
|---|---|
| OAuth server | Authenticates external apps and issues JWT tokens containing organization, scope, and branch claims. |
| API proxy | Versioned REST endpoint that validates tokens, enforces scopes, resolves branches, and returns filtered data. |
| App management | Dashboard inside Routix to register, configure, and manage OAuth apps. |
How it fits together
External App
|
|-> POST /functions/v1/oauth-token
| -> JWT with org_id, scopes, branches
|
'--> GET /functions/v1/api/v1/{branch}/accounts
-> Verify JWT -> Check scope -> Resolve branch -> Query DB -> Return filtered fieldsKey concepts
Scopes
Every API action is protected by a scope following this pattern:
routix:<resource>:<action>| Scope | Description |
|---|---|
| routix:accounts:read | Read customers, vendors, and carriers. |
| routix:accounts:write | Create and update accounts. |
| routix:orders:read | Read orders. |
| routix:orders:write | Create and update orders. |
| routix:vehicles:read | Read vehicles. |
| routix:vehicles:write | Create and update vehicles. |
| routix:staff:read | Read staff members. |
| routix:equipment:read | Read equipment. |
| routix:planning:read | Read planning and route data. |
| routix:invoices:read | Read invoices. |
| routix:invoices:write | Create and update invoices. |
Branch scoping
The API is scoped per branch using a short public ID of 8 characters, for example RS2RDH3B. That means:
- Every API call targets exactly one branch.
- Apps can be restricted to specific branches during registration.
- Branch data remains isolated.
API versioning
| Version | Behavior |
|---|---|
| v1 | Stable, immutable field set that is safe for production integrations. |
| v2 | Superset of v1 with additional fields that may grow over time. |
Both versions run simultaneously. Use v1 for stable contracts and v2 when you need additional data.
Related pages
- API calls & examples
- Setup guide

