TEASEDocs
Essentials

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.json

Point 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-client

The 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

On this page