Docs / Guides

Error handling and error codes

The shape of Tend API errors, every error code with its meaning, and retry strategies that do not make things worse.

Last updated July 8, 2026

#Error format

Every non-2xx response from https://api.tendcomputer.com/v2 carries a JSON body with a single top-level error object. Three fields are always present: code, a stable machine-readable string; message, a human-readable explanation intended for logs; and request_id, which you should include in any support request. Validation errors add a details array listing each offending field.

JSON
{
  "error": {
    "code": "invalid_request",
    "message": "Request validation failed.",
    "request_id": "req_01JB5K8WQ2X7DT4N9YZ3MHRE6P",
    "details": [
      {"field": "target", "issue": "must be an https URL"},
      {"field": "retry.max_attempts", "issue": "must be between 1 and 25"}
    ]
  }
}

Branch on code, not on message and not solely on the HTTP status. Messages are edited for clarity from time to time; codes are part of the API contract for a given api_version (currently 2026-03-01).

#Error code reference

HTTPCodeMeaning
400invalid_requestThe request body or query parameters could not be parsed or failed validation. The details field lists the offending fields.
400invalid_cron_expressionThe cron expression is malformed, or the timezone is not a valid IANA zone name.
401missing_api_keyNo Authorization header was sent, or it was not in the form Bearer <key>.
401invalid_api_keyThe key does not exist, has been revoked, or does not begin with tnd_live_ or tnd_dev_.
403insufficient_scopeThe key is valid but lacks the scope required for this operation, such as writing schedules with a read-only key.
404resource_not_foundThe job, schedule, or run ID does not exist in this project and environment. Keys from tnd_dev_ and tnd_live_ cannot see each other's resources.
409idempotency_conflictAn Idempotency-Key was reused within 24 hours with a different request body.
409schedule_name_takenA schedule with this name already exists in the project. Schedule names are unique per environment.
413payload_too_largeThe job payload exceeds 256 KB after JSON encoding.
422interval_too_shortThe schedule would fire more often than once every 30 seconds, which is the minimum interval.
422run_at_in_pastThe requested run_at time is more than 60 seconds in the past. Small clock skew is tolerated; yesterday is not.
429rate_limit_exceededThe project exceeded its requests-per-minute limit. Retry after the number of seconds in the Retry-After header.
429quota_exceededThe monthly run quota for a Hobby project has been reached. Paid plans are billed for overage instead and never receive this error.
503region_unavailableThe requested region is temporarily unable to accept new jobs. Retry, or omit region to route to the default.

#Retryable versus permanent errors

Roughly speaking, 4xx means your request is wrong and 5xx means ours is. The exceptions matter, because they determine whether a retry helps or merely repeats a mistake at scale.

  • Never retry unchanged: invalid_request, invalid_cron_expression, missing_api_key, invalid_api_key, insufficient_scope, resource_not_found, payload_too_large, interval_too_short, run_at_in_past, schedule_name_taken. The same request will fail the same way.
  • Retry after waiting: rate_limit_exceeded (honor Retry-After) and region_unavailable (use backoff).
  • Retry only if you changed something: idempotency_conflict means you reused a key with a different body. Fix the key or the body, not the timing.
  • Do not retry until the month rolls over or you upgrade: quota_exceeded. Hobby projects have a hard cap of 10,000 runs per month, and no amount of backoff will create more.

#Idempotency

Networks fail in the least convenient place: after the server did the work but before you heard about it. To make retries safe, send an Idempotency-Key header on POST requests. If Tend has already processed a request with that key within the last 24 hours and the body is identical, it returns the original response instead of creating a second job. If the body differs, you receive 409 idempotency_conflict.

Choose keys that are stable for the logical operation, not random per attempt. A key derived from your own business identifier, such as welcome-email-usr_4471, survives process restarts. A fresh UUID generated inside the retry loop defeats the purpose entirely.

cURL
curl -X POST https://api.tendcomputer.com/v2/jobs \
  -H "Authorization: Bearer $TEND_API_KEY" \
  -H "Idempotency-Key: welcome-email-usr_4471" \
  -H "Content-Type: application/json" \
  -d '{"run_at": "2026-07-09T09:00:00Z", "target": "https://app.example-shop.dev/hooks/welcome", "payload": {"user": "usr_4471"}}'

#Handling errors in code

The SDKs raise typed exceptions that expose code, status, request_id and, where present, details. Catch the specific classes you can act on and let everything else propagate. The following example retries only the errors that are worth retrying, using exponential backoff with full jitter, the same strategy Tend uses for job retries.

Python
import random, time
from tend import Tend, TendError

client = Tend(api_key=os.environ["TEND_API_KEY"], max_retries=0)
RETRYABLE = {"rate_limit_exceeded", "region_unavailable"}

def create_job(params, attempts=6, base=1.0, cap=30.0):
    for n in range(attempts):
        try:
            return client.jobs.create(**params)
        except TendError as e:
            if e.code not in RETRYABLE or n == attempts - 1:
                raise
            delay = e.retry_after or random.uniform(0, min(cap, base * 2 ** n))
            time.sleep(delay)

#Job failures versus API errors

API errors describe problems with a request to Tend. A job failure is a different animal: it means a run started and your handler did not succeed, either by returning a non-2xx status, exceeding the 15-second webhook timeout, or running past the 15-minute maximum job runtime. Job failures never appear as HTTP errors on the API, because the API call that created the job succeeded long ago.

Failed runs are retried automatically, five attempts by default and up to 25 with retry.max_attempts. The delay before retry n is a random value between 0 and min(cap, base * 2^n), where base is 10 seconds and cap is 3600 seconds by default, and both can be overridden per job (the cap within 1 to 86400 seconds). Signed webhook deliveries of results are retried for up to 48 hours. Inspect any run with GET /runs/{id}, whose attempts array records the status code, latency and truncated response body of each try.

JSON
{
  "id": "run_01JB6C3XN8Q4ZT7WK2DFY9MHAS",
  "job_id": "job_01JB6C2R5V1E8GP4X0NTQ7YKDW",
  "status": "retrying",
  "attempt": 3,
  "max_attempts": 5,
  "next_attempt_at": "2026-07-08T14:22:51Z",
  "attempts": [
    {"n": 1, "status_code": 502, "latency_ms": 340},
    {"n": 2, "status_code": 503, "latency_ms": 291},
    {"n": 3, "status_code": null, "error": "timeout_15s", "latency_ms": 15000}
  ]
}

#Getting help

When something looks wrong, collect the request_id from the error body (or the Tend-Request-Id response header) and the relevant job or run ID. Hobby projects can post in the community forum; Pro customers can write to support@tendcomputer.com and expect a response within one business day; Scale customers get a four-hour response target on business days. A request ID lets us find your exact request in seconds, which is considerably faster than a description of what you remember happening.