"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:
- Authorise — redirect the user to the authorisation endpoint.
- User consents — the user selects projects, environments, and permissions.
- Exchange code — your app exchanges the authorisation code for tokens.
- 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: trueflag with anallProjectsPermissionsarray. 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();