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.
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.
- Previous
- Troubleshooting & FAQ
- Next
- Querying data