TEASEDocs
Essentials

Response structure

JSON conventions, timestamps, the error shape, the full HTTP status table, and pagination — documented once for the whole API.

Every TEASE endpoint speaks the same dialect: JSON in, JSON out, the same error shape, the same status codes, and the same pagination contract. This page documents those conventions once so each endpoint page can focus on the fields that are unique to it.

Exact per-endpoint field lists live in the API Reference. This page is the envelope those fields travel inside — read it first, then treat each reference page as a delta on top of it.

Format

  • Every response is application/json, including errors. There is no XML, form, or protobuf encoding.
  • Requests that send a body must set Content-Type: application/json. A malformed or non-JSON body is rejected with 400.
  • Authentication is a bearer token on every request — see Authentication. A request with no valid token never reaches the resource and returns 401.
  • Field names are snake_case. Money values are decimal numbers in the account currency (for example 2370.50), not integer cents.
  • Successful reads return either a single resource object or a list of resource objects. Mutations return the resulting resource (or, where there is nothing to return, an empty body with a 2xx status).
{
  "provider_id": "lnk_8fq2",
  "name": "instagram-bio",
  "active": true,
  "created_at": 1751280000
}

Timestamps

All timestamps are Unix epoch seconds (UTC) — a plain integer, never a string and never milliseconds — unless a field is explicitly documented otherwise.

ValueMeaning
1751280000An absolute moment in time, in seconds since 1970-01-01T00:00:00Z.

To render one, multiply by 1000 for JavaScript Date, or divide nothing for most other languages:

const created = new Date(resource.created_at * 1000);
from datetime import datetime, timezone

created = datetime.fromtimestamp(resource["created_at"], tz=timezone.utc)

Because timestamps are UTC epoch seconds, you never have to parse a timezone or a date string. Convert at the edge of your app, store the integer.

Errors

Errors use the appropriate HTTP status code and a small, predictable JSON body with two fields:

{
  "error": "invalid_scope",
  "message": "Invalid scope values: ['admin']. Allowed: ['read', 'write']"
}
FieldTypeRequiredDescription
errorstringyesA short, stable, machine-readable code. Branch your logic on this.
messagestringyesA human-readable explanation. Show it to operators; do not parse it.

The golden rule: switch on the HTTP status and the error code, never on the message text. The message is for humans and may change wording at any time; the status and error code are the contract.

A non-2xx status always carries this { error, message } shape. Do not assume an error body matches the success schema of the endpoint you called — check the status first, then parse.

HTTP status codes

The API uses standard HTTP semantics. Every endpoint draws from this table; individual reference pages call out any status that carries a special meaning for that route.

StatusCode classWhen you see itWhat to do
400Bad requestThe request body or query failed validation.Read error / message, fix the payload, retry. Do not blindly retry — it will fail again.
401UnauthorizedMissing, malformed, or invalid bearer token.Check the Authorization header. If the key was revoked, mint a new one.
403ForbiddenThe token is valid but lacks the scope or permission for this resource.Use a key with the required scope (for example a write key for a mutation).
404Not foundThe resource does not exist or is not owned by your account.Confirm the id. A resource owned by another account is indistinguishable from one that never existed — this is intentional.
409ConflictThe request collides with current state (for example a uniqueness or state-machine constraint).Re-read current state, reconcile, then retry.
429Too many requestsYou exceeded a rate limit.Back off and retry — see Rate limits.

404 hides ownership

A resource that belongs to a different account returns 404, exactly like one that does not exist. This is a deliberate tenant-isolation property: you can never probe whether another account's id is real.

Reading an error in code

const res = await fetch(url, { headers });
if (!res.ok) {
  const { error, message } = await res.json();
  // branch on `error`, not on `message`
  if (res.status === 429) return backoffAndRetry();
  if (error === 'invalid_scope') throw new Error('use a write-scoped key');
  throw new Error(`${res.status} ${error}: ${message}`);
}
const data = await res.json();
res = requests.get(url, headers=headers)
if not res.ok:
    body = res.json()
    error, message = body["error"], body["message"]
    if res.status_code == 429:
        return backoff_and_retry()
    if error == "invalid_scope":
        raise RuntimeError("use a write-scoped key")
    raise RuntimeError(f"{res.status_code} {error}: {message}")
data = res.json()

Pagination

Some list endpoints accept a limit parameter to bound how many items come back in one response. Where an endpoint paginates, its exact parameters appear on its reference page — there is no single pagination scheme you can assume applies everywhere, so always read the endpoint you are calling.

FieldTypeRequiredDescription
limitintegernoMaximum items to return in one response. An endpoint that accepts it applies a sensible default and a hard maximum if you omit it or exceed the cap.
cursorstringnoWhere an endpoint pages by cursor: an opaque pointer to the next page, returned by the previous one. Pass it back verbatim — do not construct or decode it.
offsetintegernoWhere an endpoint pages by offset instead: a zero-based starting index.

Two rules keep your client correct regardless of which scheme a given endpoint uses:

  1. Page until the result set is exhausted rather than assuming a fixed page size. The server may return fewer items than your limit even when more pages remain.
  2. Treat a cursor as opaque. Its contents are an implementation detail and may change; never parse, edit, or persist meaning into it. Store only "the next cursor I was given."
// Cursor-based: follow the cursor until it stops coming back.
let cursor: string | undefined;
const all = [];
do {
  const url = new URL('https://app.tease.link/api/admin/inbox/conversations');
  url.searchParams.set('limit', '100');
  if (cursor) url.searchParams.set('cursor', cursor);
  const page = await (await fetch(url, { headers })).json();
  all.push(...page.items);
  cursor = page.next_cursor; // undefined / null when exhausted
} while (cursor);
# Offset-based: advance until a short page tells you you're done.
limit, offset, all_items = 100, 0, []
while True:
    page = requests.get(
        "https://app.tease.link/api/admin/vault/search",
        headers=headers,
        params={"limit": limit, "offset": offset},
    ).json()
    items = page["items"]
    all_items.extend(items)
    if len(items) < limit:
        break
    offset += limit

Prefer cursor pagination where an endpoint offers it: it stays correct even if rows are inserted or removed between pages, whereas offset pagination can skip or repeat items under concurrent writes.

Next steps

On this page