Authentication
Teloring uses OAuth 2.0 client credentials. You hold a long-lived client id and secret; you exchange them for a short-lived access token; you send that token on every request.
client id + secret ──POST /v1/oauth/token──▶ access token (1 hour)
│
Authorization: Bearer ────┘
The split is the point. A leaked access token expires by itself within the hour. A leaked secret is revoked in one click, without you redeploying anything.
Creating a credential
Settings → API → New credential. You choose four things:
| Name | For you. It shows up in the audit log next to everything this credential does, so name it after the integration, not after a person. |
| Expiration | A date, or unlimited. A date is worth setting for anything temporary — a migration script, a contractor's integration. |
| Scopes | Which feature areas it may reach. See Scopes. |
| SSO settings | Only if you ticked the sso scope: which agents it may sign in as, and which sites may embed the session. |
You get back a client id (tlc_…, visible forever) and a client secret
(tls_…, shown once).
It is stored only as a bcrypt hash. Nobody — not an Owner, not Teloring support — can retrieve it afterwards. Lost it? Revoke the credential and create another.
Getting a token
POST /v1/oauth/token accepts your credentials three ways. Use whichever your
HTTP client makes easiest; they are equivalent.
JSON body
curl -X POST https://api.teloring.com/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{"client_id": "tlc_…", "client_secret": "tls_…"}'
Form-encoded (what most OAuth libraries send)
curl -X POST https://api.teloring.com/v1/oauth/token \
-d grant_type=client_credentials \
-d client_id=tlc_… \
-d client_secret=tls_…
HTTP Basic (RFC 6749 §2.3.1)
curl -X POST https://api.teloring.com/v1/oauth/token \
-u "tlc_…:tls_…" \
-d grant_type=client_credentials
If both a Basic header and body parameters are present, the header wins — a body parameter cannot downgrade it.
Cache the token
One token per hour is the expected pattern. The token endpoint is rate limited to 20 requests per minute per IP, and ten consecutive failures against one client id lock that client out for fifteen minutes.
A minimal client, in the shape most people end up writing:
import time, requests
class Teloring:
BASE = "https://api.teloring.com/v1"
def __init__(self, client_id, client_secret):
self._id, self._secret = client_id, client_secret
self._token, self._expires_at = None, 0
def _auth_header(self):
# Refresh a minute early so a request never races the expiry.
if not self._token or time.time() > self._expires_at - 60:
response = requests.post(
f"{self.BASE}/oauth/token",
json={"client_id": self._id, "client_secret": self._secret},
timeout=10,
)
response.raise_for_status()
payload = response.json()
self._token = payload["access_token"]
self._expires_at = time.time() + payload["expires_in"]
return {"Authorization": f"Bearer {self._token}"}
def get(self, path, **params):
response = requests.get(f"{self.BASE}{path}", headers=self._auth_header(),
params=params, timeout=30)
if response.status_code == 401: # revoked or edited mid-flight
self._token = None
response = requests.get(f"{self.BASE}{path}", headers=self._auth_header(),
params=params, timeout=30)
response.raise_for_status()
return response.json()
The 401 retry matters more than it looks. Editing a credential's scopes, or
revoking it, invalidates outstanding tokens immediately — so a token can stop
working before its stated expiry, and the correct response is to fetch a new one
once, not to crash.
Using the token
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…
Not sure what a token can do? GET /v1/oauth/introspect tells you the account,
the credential, and exactly which scopes it holds. It is the fastest way to
explain a surprising 403.
When authentication fails
| Status | code | What happened | What to do |
|---|---|---|---|
| 400 | invalid_request | A credential field was missing | Send both client_id and client_secret |
| 400 | unsupported_grant_type | grant_type was not client_credentials | Only that grant is supported |
| 401 | invalid_client | Unknown client id, wrong secret, revoked credential, or one past its expiry date | Check the pair; check the credential in Settings → API |
| 401 | missing_token | No Authorization: Bearer header | Add the header |
| 401 | invalid_token | Token expired, tampered with, or its credential was revoked or edited | Fetch a new token |
| 402 | plan_limit_exceeded | The account's plan does not include API access | Upgrade the plan |
| 403 | ip_not_allowed | The account restricts access by IP and yours is not on the list | Add your server's outbound IP under Settings → Security & login |
| 429 | too_many_failed_attempts | Ten consecutive failures on one client id | Wait fifteen minutes; check the secret |
| 429 | rate_limit_exceeded | Too many token requests from this IP | Cache the token |
Unknown client id, wrong secret, revoked and expired all answer the same
401 invalid_client. Distinguishing them would let somebody enumerate which
client ids exist. Our logs record the real reason — quote the request_id from
the response if you need us to look.
IP restrictions
If the account has an IP allow-list under Settings → Security & login, it applies to the API as well as to sign-in. That list is described in the console as "who may access the system", and an integration holding a credential is access to the system.
When one is set:
POST /v1/oauth/tokenrefuses a call from an address that is not on the list and issues no token at all.- Every authenticated request is checked again, not just the token exchange. A token already in hand stops working the moment it is used from a disallowed address — a one-hour window during which a stolen token still worked would be a weaker promise than the one Settings makes.
Both answer 403:
{
"error": {
"type": "permission_error",
"code": "ip_not_allowed",
"message": "This IP address is not on the account's allow-list. Add it under Settings → Security & login, or call the API from an allowed address.",
"details": { "client_ip": "203.0.113.55" },
"request_id": "req_5f2a91c0e8b74d3a9c1e"
}
}
The address we compare is your server's outbound IP as it reaches us, which
is often not the address of the machine running your code — a NAT gateway, a
load balancer or an egress proxy usually sits in between. details.client_ip
in the error tells you exactly what we saw, so add that.
Both single addresses and CIDR ranges are accepted in the console:
198.51.100.7
192.0.2.0/24
An account with an empty list is unrestricted, which is the default.
If your integration runs somewhere with a changing outbound address (most
serverless platforms, some CI runners), either give it a static egress IP or
leave the allow-list empty. A partially-correct list produces intermittent
403s that are miserable to debug.
Rotating a secret
There is no in-place rotation, on purpose: a credential that can change its own secret is a credential whose secret can be changed by whoever holds it.
The zero-downtime rotation is two credentials:
- Create a second credential with the same scopes.
- Deploy the new client id and secret to your integration.
- Watch Last used on the old credential in Settings → API until it stops moving.
- Revoke the old one.
Keeping the secret safe
- Store it in a secrets manager or an environment variable — never in source control, never in a frontend bundle, never in a URL.
- Give each integration its own credential with its own scopes. One shared secret across five systems means a leak anywhere is a leak everywhere, and the audit log cannot tell you which system did what.
- Set an expiry on anything temporary.
- Revoke immediately if you suspect exposure. Revocation kills outstanding access tokens as well as the secret.