SDKs & tooling
Thin typed wrappers for Node and Python that centralize the base URL, bearer auth, retry/backoff, and pagination — plus the published OpenAPI spec for Postman, Insomnia, and code generators.
There is no heavyweight SDK to install — the API is plain JSON over HTTPS, and a few dozen
lines wrap everything you need. This page gives you a small typed client for Node and Python
that centralizes the four things every caller repeats: the base URL, the bearer header,
retry-with-backoff on 429, and pagination. Copy it, adapt it, own it.
Base URL for every request: https://app.tease.link. Authenticate with an
al_live_* bearer token — see Authentication.
A thin typed client
The wrapper does one job: never repeat boilerplate. It holds the base URL and token, retries
transient 429s with exponential backoff and jitter, raises a typed error that carries the
stable error code, and exposes a paginate helper that follows a list endpoint to
exhaustion.
// tease.ts — a thin typed wrapper. No dependencies beyond fetch.
export class TeaseError extends Error {
constructor(
public status: number,
public code: string,
message: string,
) {
super(message);
this.name = 'TeaseError';
}
}
export class Tease {
constructor(
private token: string,
private baseUrl = 'https://app.tease.link',
) {}
async request<T>(path: string, init: RequestInit = {}, tries = 5): Promise<T> {
const url = path.startsWith('http') ? path : `${this.baseUrl}${path}`;
for (let i = 0; i < tries; i++) {
const res = await fetch(url, {
...init,
headers: {
Authorization: `Bearer ${this.token}`,
'Content-Type': 'application/json',
...init.headers,
},
});
if (res.status === 429 && i < tries - 1) {
// exponential base (250ms, 500ms, 1s, …) + up to 250ms of jitter
await new Promise((r) => setTimeout(r, 2 ** i * 250 + Math.random() * 250));
continue;
}
if (!res.ok) {
const body = await res.json().catch(() => ({ error: 'unknown', message: res.statusText }));
// branch on status + error code, never on message text
throw new TeaseError(res.status, body.error, body.message);
}
return res.status === 204 ? (undefined as T) : ((await res.json()) as T);
}
throw new TeaseError(429, 'rate_limited', 'still rate limited after retries');
}
get<T>(path: string) {
return this.request<T>(path);
}
post<T>(path: string, body: unknown) {
return this.request<T>(path, { method: 'POST', body: JSON.stringify(body) });
}
// Follow a paginated list endpoint to exhaustion.
async *paginate<T>(path: string, limit = 100): AsyncGenerator<T> {
let cursor: string | undefined;
do {
const url = new URL(`${this.baseUrl}${path}`);
url.searchParams.set('limit', String(limit));
if (cursor) url.searchParams.set('cursor', cursor);
const page = await this.request<{ items: T[]; next_cursor?: string }>(url.toString());
yield* page.items;
cursor = page.next_cursor; // undefined / null when exhausted
} while (cursor);
}
}// usage
const tease = new Tease(process.env.TEASE_API_KEY!);
const link = await tease.post('/api/admin/smart-links', { name: 'instagram-bio' });
for await (const convo of tease.paginate('/api/admin/inbox/conversations')) {
console.log(convo);
}# tease.py — a thin typed wrapper. Only dependency is requests.
import os, random, time, requests
class TeaseError(Exception):
def __init__(self, status: int, code: str, message: str):
super().__init__(f"{status} {code}: {message}")
self.status = status
self.code = code
class Tease:
def __init__(self, token: str, base_url: str = "https://app.tease.link"):
self.token = token
self.base_url = base_url
def request(self, method: str, path: str, tries: int = 5, **kwargs):
url = path if path.startswith("http") else f"{self.base_url}{path}"
headers = {"Authorization": f"Bearer {self.token}", **kwargs.pop("headers", {})}
for i in range(tries):
res = requests.request(method, url, headers=headers, **kwargs)
if res.status_code == 429 and i < tries - 1:
# exponential base (0.25s, 0.5s, 1s, …) + up to 0.25s of jitter
time.sleep(2 ** i * 0.25 + random.random() * 0.25)
continue
if not res.ok:
body = res.json() if res.content else {}
# branch on status + error code, never on message text
raise TeaseError(res.status_code, body.get("error", "unknown"), body.get("message", ""))
return res.json() if res.content else None
raise TeaseError(429, "rate_limited", "still rate limited after retries")
def get(self, path: str, **kwargs):
return self.request("GET", path, **kwargs)
def post(self, path: str, json: dict, **kwargs):
return self.request("POST", path, json=json, **kwargs)
def paginate(self, path: str, limit: int = 100):
cursor = None
while True:
params = {"limit": limit}
if cursor:
params["cursor"] = cursor
page = self.request("GET", path, params=params)
yield from page["items"]
cursor = page.get("next_cursor") # None when exhausted
if not cursor:
break# usage
tease = Tease(os.environ["TEASE_API_KEY"])
link = tease.post("/api/admin/smart-links", {"name": "instagram-bio"})
for convo in tease.paginate("/api/admin/inbox/conversations"):
print(convo)This wrapper already encodes the two rules every robust client needs: retry 429 with
backoff (see Rate limits) and branch on the stable error
code, never the human-readable message (see Errors).
Download the OpenAPI spec
The full API is published as an OpenAPI document. Once your instance is deployed, the spec is served at:
https://app.tease.link/openapi.jsonPoint any OpenAPI-aware tool at that URL — or save it to a file — to get an interactive client without writing one:
- Postman / Insomnia — import the spec to get a ready-made request collection with every endpoint, its parameters, and the bearer auth slot pre-wired.
- openapi-generator — generate a typed client in your language of choice straight from the spec, so the request/response models stay in sync with the API.
# fetch the spec
curl https://app.tease.link/openapi.json -o tease-openapi.json
# generate a typed client (example: TypeScript)
openapi-generator-cli generate -i tease-openapi.json -g typescript-fetch -o ./tease-clientThe spec describes the same endpoints documented in the API Reference. Whichever you start from — generated client or the thin wrapper above — the base URL and bearer auth are identical.
Next steps
- Authentication — mint and use an
al_live_*token. - Errors — the stable
errorcodes your client switches on. - Rate limits — back off correctly on
429. - Response structure — the JSON envelope and pagination.