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 with400. - 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 example2370.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
2xxstatus).
{
"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.
| Value | Meaning |
|---|---|
1751280000 | An 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']"
}| Field | Type | Required | Description |
|---|---|---|---|
error | string | yes | A short, stable, machine-readable code. Branch your logic on this. |
message | string | yes | A 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.
| Status | Code class | When you see it | What to do |
|---|---|---|---|
400 | Bad request | The request body or query failed validation. | Read error / message, fix the payload, retry. Do not blindly retry — it will fail again. |
401 | Unauthorized | Missing, malformed, or invalid bearer token. | Check the Authorization header. If the key was revoked, mint a new one. |
403 | Forbidden | The 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). |
404 | Not found | The 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. |
409 | Conflict | The request collides with current state (for example a uniqueness or state-machine constraint). | Re-read current state, reconcile, then retry. |
429 | Too many requests | You 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.
| Field | Type | Required | Description |
|---|---|---|---|
limit | integer | no | Maximum 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. |
cursor | string | no | Where 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. |
offset | integer | no | Where 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:
- Page until the result set is exhausted rather than assuming a fixed page size. The
server may return fewer items than your
limiteven when more pages remain. - Treat a
cursoras 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 += limitPrefer 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
- Authentication — mint and use an
al_live_*token. - Rate limits — what
429means and how to back off. - API Reference — the exact fields for every endpoint.