TASKCLANQuickstart ↗

REFERENCE

Errors & rate limits

The API uses conventional HTTP status codes and returns a structured error body. Build for failure: retry transient errors with backoff, and surface the rest clearly.

Error shape

{
  "error": {
    "type": "rate_limit_error",
    "code": "too_many_requests",
    "message": "You have exceeded your requests-per-minute limit.",
    "request_id": "req_a1b2c3"
  }
}

Always log request_id, it lets us trace a specific call if you contact support.

Status codes

StatusMeaningWhat to do
400Invalid requestFix the payload; do not retry unchanged.
401Bad or revoked keyCheck the API key.
402Insufficient creditsTop up the wallet (see below). Do not retry unchanged.
403Not permittedKey lacks scope for this action.
404Not foundCheck the resource or endpoint.
429Rate limitedBack off and retry (see below).
500 / 503Server error / overloadedRetry with backoff.

Insufficient credits (402)

Metered T1 calls debit your wallet. When it's empty the API returns 402 with the failing balance and a URL to top up:

{
  "error": "insufficient_credits",
  "balance": 0,
  "topUpUrl": "https://platform.taskclan.com/billing/recharge",
  "requestId": "req_a1b2c3"
}

The SDK wraps this as a TaskclanError whose type is "insufficient_credits" and whose balance and topUpUrl are populated. The recommended catch, safe across realms and bundlers (no instanceof required):

import { TaskclanError } from "@taskclan/sdk";

try {
  await taskclan.run({ goal: "…" });
} catch (e) {
  if (TaskclanError.isInsufficientCredits(e)) {
    // e.balance, e.topUpUrl, e.requestId are all populated.
    // Prompt the operator to top up, then retry when the wallet is funded.
    return promptTopUp(e.topUpUrl);
  }
  throw e;
}

In a browser app, redirect to e.topUpUrl (or open it in a new tab) so the user lands on the recharge page. In a headless worker, page the operator — a 402 will not clear itself on retry.

Do not retry a 402 with the same key. A failed call is never billed, but a retry loop against an empty wallet just races against the top-up. Back off and wait for the balance to change (poll GET /api/t1/credits/balance).

Rate limits

Limits are applied per key. Every response includes headers so you can pace requests:

HeaderMeaning
x-ratelimit-limitRequests allowed in the window
x-ratelimit-remainingRequests left in the window
x-ratelimit-resetSeconds until the window resets

Retrying with backoff

Retry 429, 500 and 503 with exponential backoff and jitter. The SDK does this for you by default; here is the shape:

async function withRetry(fn, tries = 4) {
  for (let i = 0; i < tries; i++) {
    try {
      return await fn();
    } catch (err) {
      if (!err.retryable || i === tries - 1) throw err;
      const wait = Math.min(1000 * 2 ** i, 8000) + Math.random() * 250;
      await new Promise((r) => setTimeout(r, wait));
    }
  }
}
Idempotency: send an Idempotency-Key header on writes so a retried request is not executed twice.