Errors

Error envelope, status codes, and which ones to retry.

Every error response shares the same shape:

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded (60 req/min). Retry in 12s.",
    "limit": 60,
    "current": 60
  }
}
  • code — stable machine-readable identifier. Subject to additions but never silent renames.
  • message — human-readable, may evolve. Do not parse.
  • limit / current — present only for quota errors.

Status codes

StatusCode(s)MeaningRetry?
400VALIDATION_ERRORBody or query parameter failed schema validation.No — fix the request.
401UNAUTHORIZEDMissing, malformed, expired, or revoked key.No — mint a fresh key.
403UNAUTHORIZEDResource exists but not in this key’s project.No — wrong project key.
404NOT_FOUNDFlow, run, or file doesn’t exist.No.
409RUN_NOT_RUNNINGOperation requires a running instance (e.g. POST /messages to a sleeping run).After resuming, or stop trying.
409(none)Self-hosted runner offline at trigger time.Yes, after the runner reconnects.
429RATE_LIMITEDPer-key request rate exceeded. Respect Retry-After.Yes, with backoff.
429CONCURRENT_LIMITProject at its concurrent-runs cap.Yes, once a run finishes.
429MONTHLY_QUOTAProject hit its monthly run quota.Not until next month, or upgrade.
500(none)Bug on our side.Yes, with backoff.
503UPSTREAM_UNAVAILABLEContainer is up but the WS upstream isn’t accepting input (brief race).Yes, immediately.

Suggested retry policy

attempt 1 → fail → wait 1 s
attempt 2 → fail → wait 2 s
attempt 3 → fail → wait 4 s
attempt 4 → fail → give up

Only apply this to retryable statuses (429, 500, 503, network errors). Use the Retry-After header on 429 instead of your own backoff when it’s present.

For 429 RATE_LIMITED specifically, the X-RateLimit-Reset header tells you the epoch seconds when the bucket will refill — you can wait until then exactly.

Typed errors in the SDK

import {
  FlowKoiAuthError,
  FlowKoiNotFoundError,
  FlowKoiRateLimitError,
  FlowKoiValidationError,
  FlowKoiUpstreamError,
} from "@flowkoi/sdk";

try {
  await flowkoi.flows.run(flowId, { prompt });
} catch (err) {
  if (err instanceof FlowKoiRateLimitError) {
    await sleep(err.retryAfterSeconds * 1000);
    return retry();
  }
  if (err instanceof FlowKoiNotFoundError) return notify("Flow has been deleted");
  if (err instanceof FlowKoiAuthError)     return notify("API key rotated; refresh");
  throw err;
}

Every typed error carries the raw body from the response, so you can still introspect err.body?.error?.code if you need a code we haven’t given a dedicated class to yet.

Error logs in the dashboard

Every API call is recorded in the per-key daily usage aggregates (count + errors + total duration). The API tab in Project Settings renders a 30-day chart so you can spot regressions without scraping logs.