Thread Docs

Errors & rate limits

The error envelope, every status code, and how rate limiting behaves.

Error envelope

Every error response has the same shape:

{ "error": { "code": "FORBIDDEN", "message": "Access denied by policy" } }

The code is stable and machine-readable; the message is for humans and may change.

Gateway status codes

StatuscodeWhen
400VALIDATION_ERRORMissing/invalid params — e.g. no connectionId/accountId, or no sender domain.
401UNAUTHORIZEDMissing or invalid API key.
403FORBIDDENThe grant doesn't allow this call, or it's outside its schedule.
403NO_ACTIVE_GRANTThe accountId has no active grant — the user revoked it, or it expired. Sever local state and re-run Connect (you also received a connection.revoked webhook).
404NOT_FOUNDThe connection doesn't exist.
429RATE_LIMITEDThe grant's daily limit is exhausted.
501NOT_IMPLEMENTEDNot available yet (attachment download).
503ENGINE_ERRORThe permission engine hit a malformed rule (fail-closed).
503PROVIDER_ERRORThe upstream mail provider failed (fail-closed).

Connect token codes

Returned by exchangeConnectCode (POST /connect/token):

StatuscodeWhen
400INVALID_GRANTThe code is expired (5-min TTL) or already used.
401INVALID_CLIENTBad client_id / client_secret.

SDK error classes

The SDK throws typed errors so you can branch with instanceof instead of inspecting codes:

ClassMaps tocode
ThreadValidationError400VALIDATION_ERROR
ThreadAuthError401UNAUTHORIZED
ThreadDeniedError403FORBIDDEN or NO_ACTIVE_GRANT (check .code)
ThreadNotFoundError404NOT_FOUND
ThreadRateLimitedError429RATE_LIMITED
ThreadProviderError503PROVIDER_ERROR
ThreadWebhookSignatureError401INVALID_SIGNATURE
ThreadErrorbase class— (.code, .status)

A 501 NOT_IMPLEMENTED (and any unrecognized status) throws the base ThreadError, with .code and .status set from the response.

import { ThreadDeniedError, ThreadRateLimitedError, ThreadError } from '@jointhread/sdk';

try {
  await thread.messages.list({ accountId, fromDomain: 'amazon.com' });
} catch (err) {
  if (err instanceof ThreadDeniedError) {
    if (err.code === 'NO_ACTIVE_GRANT') {
      // Revoked or expired — sever local state for this accountId and offer reconnect.
    }
    // FORBIDDEN: the grant exists but doesn't cover this read (scope/schedule).
  } else if (err instanceof ThreadRateLimitedError) {
    // The grant's daily cap is exhausted — back off until the next UTC day.
  } else if (err instanceof ThreadError) {
    console.error(err.code, err.status);
  }
}

Rate limits

A user can cap a grant at N requests per day (rateLimitPerDay). When the cap is reached, calls return 429 RATE_LIMITED until the next UTC day.

Rate limiting is per-grant and set by the user, not a fixed platform quota. Treat a 429 as a "come back later" signal — back off and retry after the UTC day rolls over, and surface a gentle "access paused" state rather than failing hard.

Fail-closed by design

When Thread can't be sure a request is allowed — a malformed rule, a provider outage — it denies rather than guessing (503 ENGINE_ERROR / PROVIDER_ERROR). Retry 503s with backoff; never treat them as permission to proceed.