Skip to main content

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:

NameFor you. It shows up in the audit log next to everything this credential does, so name it after the integration, not after a person.
ExpirationA date, or unlimited. A date is worth setting for anything temporary — a migration script, a contractor's integration.
ScopesWhich feature areas it may reach. See Scopes.
SSO settingsOnly 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).

The secret is 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

StatuscodeWhat happenedWhat to do
400invalid_requestA credential field was missingSend both client_id and client_secret
400unsupported_grant_typegrant_type was not client_credentialsOnly that grant is supported
401invalid_clientUnknown client id, wrong secret, revoked credential, or one past its expiry dateCheck the pair; check the credential in Settings → API
401missing_tokenNo Authorization: Bearer headerAdd the header
401invalid_tokenToken expired, tampered with, or its credential was revoked or editedFetch a new token
402plan_limit_exceededThe account's plan does not include API accessUpgrade the plan
403ip_not_allowedThe account restricts access by IP and yours is not on the listAdd your server's outbound IP under Settings → Security & login
429too_many_failed_attemptsTen consecutive failures on one client idWait fifteen minutes; check the secret
429rate_limit_exceededToo many token requests from this IPCache the token
One message for every credential failure

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/token refuses 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.

Serverless and dynamic IPs

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:

  1. Create a second credential with the same scopes.
  2. Deploy the new client id and secret to your integration.
  3. Watch Last used on the old credential in Settings → API until it stops moving.
  4. 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.