"OAuth2 API"

Allow third-party applications to access your Tether projects using industry-standard OAuth2 with long-lived tokens and refresh token rotation.

Overview

Tether acts as an OAuth2 Authorisation Server. External applications can request scoped access to a user's projects and environments using the Authorization Code + PKCE grant type.

During the consent flow, users select exactly which projects, environments, and permissions to grant. Permissions are split into data scopes (per-environment: query, mutate, action, subscribe) and management scopes (per-project: settings, deploy, data, backups, crons, variables, storage, logs). Access tokens are short-lived (1 hour) and refresh tokens last 90 days with automatic rotation.

Flow:

  1. Authorise — redirect the user to the authorisation endpoint.
  2. User consents — the user selects projects, environments, and permissions.
  3. Exchange code — your app exchanges the authorisation code for tokens.
  4. Access token — use the token to make API requests.

Registering an application

Navigate to Dashboard > Developers to register a new OAuth2 application. You'll need:

  • Name — displayed to users on the consent screen
  • Redirect URIs — one or more callback URLs for your app (exact match required)
  • Description (optional) — shown on the consent screen
  • Homepage URL (optional) — link to your app

After registration, you'll receive a client_id (public) and a client_secret (shown once — store it securely).

Authorisation flow

1. Build the authorise URL

Redirect the user's browser to:

GET https://tether-api.strands.gg/oauth/authorize
  ?response_type=code
  &client_id=tthr_cid_...
  &redirect_uri=https://yourapp.com/callback
  &code_challenge=BASE64URL(SHA256(code_verifier))
  &code_challenge_method=S256
  &scope=projects:query,projects:mutate,projects:settings,userinfo
  &state=random_csrf_token

2. User consents

The user is shown the consent screen where they select which projects, environments, and permissions to grant. They can enable or disable individual permissions per environment.

3. Receive the callback

After the user approves, they're redirected to your redirect_uri:

https://yourapp.com/callback?code=tthr_oac_...&state=random_csrf_token

4. Exchange the code for tokens

curl -X POST https://tether-api.strands.gg/api/v1/oauth/token \
  -d "grant_type=authorization_code" \
  -d "code=tthr_oac_..." \
  -d "client_id=tthr_cid_..." \
  -d "client_secret=tthr_cs_..." \
  -d "redirect_uri=https://yourapp.com/callback" \
  -d "code_verifier=your_original_verifier"

Response:

{
  "access_token": "tthr_oat_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tthr_ort_...",
  "scope": "granted"
}

Using the access token

Include the access token in API requests as a Bearer token.

Running a query

curl -X POST https://tether-api.strands.gg/api/v1/projects/{project_id}/query \
  -H "Authorization: Bearer tthr_oat_..." \
  -H "Content-Type: application/json" \
  -d '{"function": "posts.list", "args": {}}'

Running a mutation

curl -X POST https://tether-api.strands.gg/api/v1/projects/{project_id}/mutate \
  -H "Authorization: Bearer tthr_oat_..." \
  -H "Content-Type: application/json" \
  -d '{"function": "posts.create", "args": {"title": "Hello from OAuth"}}'

Targeting a specific environment

Append the environment name to the path to target a specific environment:

curl -X POST https://tether-api.strands.gg/api/v1/projects/{project_id}/env/{env_name}/query \
  -H "Authorization: Bearer tthr_oat_..." \
  -H "Content-Type: application/json" \
  -d '{"function": "posts.list", "args": {}}'

The token is scoped — requests for projects or environments that weren't granted will return 403 Forbidden.

Discovery endpoints

Before calling functions, your application can discover which environments and functions are available for a project.

List environments

Returns environment names and the default environment. Available to any token with a grant for the project (no specific scope required).

curl https://tether-api.strands.gg/api/v1/projects/{project_id}/environments/names \
  -H "Authorization: Bearer tthr_oat_..."

Response:

{
  "environments": ["development", "production"],
  "defaultEnvironment": "development"
}

List functions

Returns deployed functions with their type, arguments, and access level. Requires the deploy scope.

curl https://tether-api.strands.gg/api/v1/projects/{project_id}/functions \
  -H "Authorization: Bearer tthr_oat_..."

Response:

{
  "functions": [
    {
      "name": "posts.list",
      "type": "query",
      "args": null,
      "access": "authenticated"
    },
    {
      "name": "posts.create",
      "type": "mutation",
      "args": "args: { title: z.string(), body: z.string() }",
      "access": "authenticated"
    }
  ]
}

To list functions for a specific environment, include the environment name in the path:

curl https://tether-api.strands.gg/api/v1/projects/{project_id}/env/{env_name}/functions \
  -H "Authorization: Bearer tthr_oat_..."

Scoped permissions

OAuth2 access tokens are scoped to exactly the projects, environments, and permissions the user granted during the consent flow. Permissions are split into two groups.

Data scopes (per-environment)

These are checked against a specific project and environment pair:

Scope Description
query Call query functions via POST /projects/{id}/query
mutate Call mutation functions via POST /projects/{id}/mutate
action Call action functions via POST /projects/{id}/action
subscribe Open WebSocket subscriptions for realtime updates

Management scopes (per-project)

These are checked at the project level — if any environment grant for the project includes the permission, the check passes:

Scope Description
settings Manage project settings and environment configuration
deploy Deploy functions and schema
data Access the data browser (table/row CRUD)
backups Create, list, and manage database backups
crons Manage scheduled cron jobs
variables Manage environment variables
storage Upload, download, and manage files
logs View and manage execution logs

If a token lacks the required permission for a given project/environment, the request returns 403 Forbidden with a clear error message.

Raw SQL execution (/exec) is never available via OAuth2 tokens, regardless of granted permissions. This protects against unrestricted database access from third-party applications.

Refreshing tokens

Access tokens expire after 1 hour. Use the refresh token to get a new pair. Refresh tokens rotate — each refresh returns a new refresh token and invalidates the old one.

curl -X POST https://tether-api.strands.gg/api/v1/oauth/token \
  -d "grant_type=refresh_token" \
  -d "refresh_token=tthr_ort_..." \
  -d "client_id=tthr_cid_..." \
  -d "client_secret=tthr_cs_..."

Warning: If a previously rotated refresh token is reused, all tokens for that grant are revoked for security. This protects against token theft.

Revoking tokens

curl -X POST https://tether-api.strands.gg/api/v1/oauth/revoke \
  -d "token=tthr_oat_..."

Users can also revoke all access for an app from Dashboard > Authorised Apps.

Token introspection

Inspect an access token to check whether it is still active and which scopes were granted (RFC 7662). Authenticate with your application's client credentials:

curl -X POST https://tether-api.strands.gg/api/v1/oauth/introspect \
  -d "token=tthr_oat_..." \
  -d "client_id=tthr_cid_..." \
  -d "client_secret=tthr_cs_..."

Response for an active token:

{
  "active": true,
  "scope": "projects:query projects:mutate projects:settings",
  "client_id": "tthr_cid_...",
  "token_type": "Bearer",
  "exp": 1700000000,
  "sub": "user-uuid",
  "grants": [
    {
      "projectId": "project-uuid",
      "environmentName": "production",
      "permissions": ["query", "mutate", "settings"]
    }
  ]
}

Response for an inactive or expired token:

{
  "active": false
}

User info endpoint

If the userinfo scope was granted, retrieve the authenticated user's profile:

curl https://tether-api.strands.gg/api/v1/oauth/userinfo \
  -H "Authorization: Bearer tthr_oat_..."

Response:

{
  "id": "user-uuid",
  "email": "[email protected]",
  "name": "Jane Smith"
}

Available scopes

Request these in the scope query parameter (comma-separated).

Data scopes

Scope Description
projects:query Run read-only queries
projects:mutate Run mutations (write operations)
projects:action Run actions (external side effects)
projects:subscribe Subscribe to realtime updates

Management scopes

Scope Description
projects:settings Manage project and environment settings
projects:deploy Deploy functions and schema
projects:data Access the data browser
projects:backups Manage database backups
projects:crons Manage cron jobs
projects:variables Manage environment variables
projects:storage Manage file storage
projects:logs View and manage execution logs

Other scopes

Scope Description
userinfo Access user profile (email, name)

Project scope mode

On the consent screen, the user chooses one of two modes:

  • Authorise all projects — grants the requested permissions across all current and future projects. The token carries an allProjects: true flag with an allProjectsPermissions array. This is ideal for integrations that need blanket access (e.g. CI/CD pipelines, MCP servers).
  • Authorise specific projects — the user selects individual projects and environments to grant. Data scopes are selected per-environment, while management scopes apply to the entire project.

The scope query parameter in the authorisation URL signals the app's intent — the user can then accept or narrow those permissions on the consent screen.

Token prefixes

Prefix Description
tthr_cid_ Client ID (public)
tthr_cs_ Client secret (private)
tthr_oat_ Access token
tthr_ort_ Refresh token
tthr_oac_ Authorisation code

Security notes

  • PKCE is required — only the S256 method is supported
  • Redirect URIs must be an exact match (no wildcards)
  • All tokens are stored as SHA-256 hashes — the plaintext is never persisted
  • Authorisation codes are single-use and expire after 10 minutes
  • Refresh tokens rotate on every use — reuse of an old token revokes all tokens
  • Raw SQL execution is blocked for all OAuth2 tokens

Example: Node.js integration

A complete example using fetch to connect a Node.js service to Tether via OAuth2:

import crypto from 'crypto';

// 1. Generate PKCE pair
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');

// 2. Build authorise URL (open in browser)
const authoriseUrl = new URL('https://tether-api.strands.gg/oauth/authorize');
authoriseUrl.searchParams.set('response_type', 'code');
authoriseUrl.searchParams.set('client_id', 'tthr_cid_...');
authoriseUrl.searchParams.set('redirect_uri', 'http://localhost:3000/callback');
authoriseUrl.searchParams.set('code_challenge', codeChallenge);
authoriseUrl.searchParams.set('code_challenge_method', 'S256');
authoriseUrl.searchParams.set('scope', 'projects:query,projects:mutate,projects:settings');
authoriseUrl.searchParams.set('state', crypto.randomBytes(16).toString('hex'));

// 3. After callback, exchange the code
const tokenRes = await fetch('https://tether-api.strands.gg/api/v1/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: 'tthr_oac_...',  // from callback
    client_id: 'tthr_cid_...',
    client_secret: 'tthr_cs_...',
    redirect_uri: 'http://localhost:3000/callback',
    code_verifier: codeVerifier,
  }),
});

const { access_token, refresh_token } = await tokenRes.json();

// 4. Use the token to query data
const queryRes = await fetch(
  'https://tether-api.strands.gg/api/v1/projects/{project_id}/query',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${access_token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      function: 'posts.list',
      args: { limit: 10 },
    }),
  }
);

const { data } = await queryRes.json();