API Reference
Authentication
The Tidal Control API uses Bearer tokens for authentication. Tokens are issued by your tenant's Keycloak realm using the OAuth 2.0 Device Authorization Grant (RFC 8628). This flow is designed for scripts and CLI tools that cannot open a browser window themselves, and the token acts as you — with your permissions.
If your integration runs unattended — a scheduled export into a BI tool, a server-to-server sync — use read-only machine-to-machine credentials instead. See Machine-to-machine authentication at the end of this page.
Prerequisites
You need your tenant name — the Keycloak realm identifier for your organization. It is the organization identifier in your portal URL:
https://portal.tidalcontrol.com/{your-tenant}/
For example, if your portal URL is https://portal.tidalcontrol.com/demo/, your tenant name is demo.
Throughout this guide, replace {your-tenant} with your actual tenant name.
The authentication flow
The Device Authorization Grant works in three steps:
- Your script requests a device code from Keycloak
- You (or the script user) log in via a browser URL
- Your script polls for the access token once login is complete
Step 1 — Request a device code
curl -X POST \
"https://auth.tidalcontrol.com/realms/{your-tenant}/protocol/openid-connect/auth/device" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=portal&scope=openid"
Response:
{
"device_code": "...",
"user_code": "ABCD-1234",
"verification_uri": "https://auth.tidalcontrol.com/realms/{your-tenant}/device",
"verification_uri_complete": "https://auth.tidalcontrol.com/realms/{your-tenant}/device?user_code=ABCD-1234",
"expires_in": 600,
"interval": 5
}
Step 2 — Log in via browser
Open verification_uri_complete in a browser and log in with your Tidal Control credentials. Your script waits while you do this.
Step 3 — Poll for the token
Poll the token endpoint every interval seconds until login completes:
curl -X POST \
"https://auth.tidalcontrol.com/realms/{your-tenant}/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id=portal&device_code={device_code}"
Once the user has logged in, the response contains your access token:
{
"access_token": "eyJhbGci...",
"expires_in": 300,
"refresh_token": "eyJhbGci...",
"refresh_expires_in": 1800,
"token_type": "Bearer"
}
Access tokens expire after 5 minutes. Use the refresh_token to get a new access token without re-authenticating (see below).
Making API requests
Include the access token in every GraphQL request as a Bearer header:
curl -X POST https://portal.tidalcontrol.com/graphql \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"query": "{ tenant { name displayName } }"}'
Complete Python example
This script handles the full authentication flow and saves the token for reuse:
import requests
import time
import json
TENANT = "{your-tenant}"
BASE_URL = f"https://auth.tidalcontrol.com/realms/{TENANT}/protocol/openid-connect"
CLIENT_ID = "portal"
GRAPHQL_URL = "https://portal.tidalcontrol.com/graphql"
def get_token():
# Step 1: Request device code
r = requests.post(
f"{BASE_URL}/auth/device",
data={"client_id": CLIENT_ID, "scope": "openid"},
)
r.raise_for_status()
device = r.json()
print(f"\nOpen this URL in your browser to log in:")
print(f" {device['verification_uri_complete']}\n")
# Step 2: Poll for token
interval = device.get("interval", 5)
deadline = time.time() + device["expires_in"]
while time.time() < deadline:
time.sleep(interval)
r = requests.post(
f"{BASE_URL}/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"client_id": CLIENT_ID,
"device_code": device["device_code"],
},
)
if r.status_code == 200:
token = r.json()
print("Authentication successful.")
return token
body = r.json()
if body.get("error") == "authorization_pending":
continue # Still waiting for user to log in
if body.get("error") == "slow_down":
interval += 5
continue
r.raise_for_status()
raise RuntimeError("Authentication timed out.")
def refresh_token(refresh_tok):
r = requests.post(
f"{BASE_URL}/token",
data={
"grant_type": "refresh_token",
"client_id": CLIENT_ID,
"refresh_token": refresh_tok,
},
)
r.raise_for_status()
return r.json()
def graphql(query, variables, access_token):
r = requests.post(
GRAPHQL_URL,
json={"query": query, "variables": variables},
headers={"Authorization": f"Bearer {access_token}"},
)
r.raise_for_status()
result = r.json()
if "errors" in result:
raise RuntimeError(result["errors"])
return result["data"]
if __name__ == "__main__":
token = get_token()
data = graphql("{ tenant { name displayName } }", {}, token["access_token"])
print(json.dumps(data, indent=2))
Refreshing an expired token
When the access token expires, use the refresh token instead of going through the full login flow again:
token = refresh_token(token["refresh_token"])
access_token = token["access_token"]
Refresh tokens also expire (typically after 30 minutes of inactivity). If a refresh fails, run the full get_token() flow again.
Machine-to-machine authentication
For unattended integrations that run without a person present — a nightly export into a BI tool, a scheduled server-to-server sync — use machine-to-machine (M2M) credentials instead of the device flow. An M2M credential is an OAuth 2.0 Client Credentials client: a client_id and client_secret pair that your script exchanges directly for an access token, with no browser step.
M2M credentials are read-only. They can run queries but cannot run mutations.
M2M credentials are enabled per organization. If you don't see the API Credentials page in your settings, email support@tidalcontrol.com to have it enabled.
Creating a credential
Creating credentials requires the Super User role.
- Go to Settings → API Credentials.
- Click Create credential, give it a label, and optionally set an expiry date.
- Copy the client secret immediately — it is shown once and cannot be retrieved again. Store it in a secret manager or vault, never in plaintext config.
You now have a client_id and a client_secret. Together these are your credential.
Requesting a token
Exchange the credential for an access token using the client_credentials grant. There is no browser step and no scope parameter:
curl -X POST \
"https://auth.tidalcontrol.com/realms/{your-tenant}/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id={client_id}" \
-d "client_secret={client_secret}"
Response:
{
"access_token": "eyJhbGci...",
"expires_in": 300,
"token_type": "Bearer"
}
This flow has no refresh token. When the access token expires (after about 5 minutes), request a new one with the same client_credentials call.
Include the access token in every GraphQL request as a Bearer header, exactly as in Making API requests above.
Complete shell example
Set CLIENT_SECRET to the secret you copied, then fetch a token and call the API:
export CLIENT_SECRET='<the secret you copied>'
ACCESS_TOKEN=$(curl -s -X POST \
"https://auth.tidalcontrol.com/realms/{your-tenant}/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id={client_id}" \
-d "client_secret=$CLIENT_SECRET" \
| jq -r .access_token)
curl -s https://portal.tidalcontrol.com/graphql \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ currentTenant { name } }"}'
What a credential can read
A read-only credential can read your organization's entire data set — controls, evidence, personnel, KPIs, reports, and the user directory (names and email addresses). Treat a leaked secret as full-organization data exposure and rotate it immediately.
Rotating and revoking
From Settings → API Credentials, use the row actions (kebab menu, three vertical dots) to:
- Rotate — generate a new secret and invalidate the old one. Update your integration with the new secret.
- Revoke — disable the credential permanently.
Access tokens stay valid until they expire (about 5 minutes), so a rotate, revoke, or expiry takes effect within one token lifetime. Allow for up to a 5-minute window before the change is fully enforced.
- Previous
- MCP server
- Next
- Querying data