API docs
Search docs
Guides, operations and types

Errors, limits & pagination

Predictable failures with machine-readable codes, generous limits with standard headers.

Errors

Following the GraphQL convention, a request that reached the API answers HTTP 200 and lists problems in errors. Branch on extensions.code, never on the message text.

Response
{
  "errors": [
    {
      "message": "this API key lacks the SETS_WRITE scope",
      "path": ["editEmoteSet"],
      "extensions": { "code": "missing_scope" }
    }
  ],
  "data": null
}
CodeMeaning
unauthorizedThe field needs an API key or session.
missing_scopeThe key lacks the scope named in the message.
forbiddenAuthenticated, but not allowed (not your emote, not an editor, staff only…).
not_foundThe target doesn't exist or isn't visible to you.
rate_limitedToo many changes; retry later.
invalid_name, invalid_alias, …Input validation; the message explains the rule.
internalOur fault. Details are logged on our side and never leaked; retry with backoff.

Single-object lookups like emote(id:) return null instead of an error when nothing is found. A malformed or unknown query fails validation with a descriptive message and HTTP 422. Request bodies are capped at 1 MB.

HTTP-level failures carry a JSON body {"error":{"code","message"}}:

StatusWhen
401Invalid, expired or revoked API key (invalid_api_key).
429Rate limit exceeded; see Retry-After.

Rate limits

LimitBudgetApplies to
Per IP address600 requests / minuteEvery request (IPv6 is grouped per /64)
Per API key1,200 requests / minuteAll requests with that key, from any IP
Changes per account120 mutations / minuteEvery mutation field, keys and sessions combined
Uploads20 / hourEmote uploads per account

Responses carry the IETF RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers (seconds until the window resets), plus Retry-After on 429. Need more for a large integration? Batch: one editEmoteSet call applies up to 500 changes and counts as one mutation.

Query cost

Each query is priced before it runs: every field costs 1, and list fields multiply the cost of their children by the page size you ask for. Queries above 5,000 are rejected with a COMPLEXITY_LIMIT_EXCEEDED validation error. In practice: fetch what you display, and don't nest large lists inside large lists.

Pagination

Lists that can grow without bound use cursors. Pass first (1–100) and the previous page's endCursor as after until hasNextPage is false. Cursors are stable: new emotes don't shift pages you already fetched.

GraphQL
query Page($after: String) {
  emotes(first: 100, after: $after) {
    nodes { id name }
    pageInfo { endCursor hasNextPage }
  }
}
JavaScript
let after = null;
do {
  const { data } = await gql(PAGE_QUERY, { after });
  handle(data.emotes.nodes);
  after = data.emotes.pageInfo.hasNextPage ? data.emotes.pageInfo.endCursor : null;
} while (after);

searchEmotes is ranked by relevance and therefore uses page numbers (page, perPage up to 100). Its total is -1 while search runs in degraded mode and the total is unknown.

Search
Find emotes or jump to a page