Errors, Retries, and Rate Limits
All errors return JSON with a single error field:
{ "error": "human-readable error message" }Use the HTTP status code and message together to decide whether to retry, stop, or surface the error to a human.
Status codes
Section titled “Status codes”| Status | Meaning | Retry? |
|---|---|---|
400 | Invalid request, bad input, unknown fields, or validation failure | No, fix the request |
401 | Missing or invalid authentication | No, refresh credentials |
403 | Insufficient permissions for the tenant or role | No, use a key with the right role |
404 | Resource not found | No, fix the path or wait for async creation |
409 | Resource already exists | No, check the existing resource |
412 | ETag mismatch because the resource changed after you read it | Yes, re-fetch and retry |
429 | Rate limited | Yes, wait Retry-After seconds |
500 | Internal server error | Yes, with backoff, and surface it if it persists |
503 | Service unavailable, for example while a cluster is still becoming ready | Yes, with backoff |
Rate limiting
Section titled “Rate limiting”The API enforces rate limits per IP address. When you exceed the limit you get a 429 response with a Retry-After header.
Retry-After is the number of seconds to wait before retrying.
HEADERS=$(mktemp)STATUS=$(curl -s -D "$HEADERS" -o /tmp/body -w "%{http_code}" \ -H "Authorization: Bearer $KUPE_API_KEY" \ "https://api.kupe.cloud/api/v1/tenants/<tenant>/clusters")
if [ "$STATUS" = "429" ]; then WAIT=$(awk -F': ' 'tolower($1)=="retry-after" {gsub(/\r/,""); print $2}' "$HEADERS") echo "Rate limited, waiting ${WAIT}s" sleep "${WAIT:-5}"fiRepeated authentication failures trigger exponential backoff on top of the regular limit. Verify your API key is correct before retrying in a loop.
Optimistic locking conflicts
Section titled “Optimistic locking conflicts”PATCH requests that include an If-Match header fail with 412 Precondition Failed if the resource changed after you read it. This is recoverable: re-fetch the resource, apply your change against the new state, and retry.
for attempt in 1 2 3; do ETAG=$(curl -sD - -o /dev/null \ -H "Authorization: Bearer $KUPE_API_KEY" \ "https://api.kupe.cloud/api/v1/tenants/<tenant>/clusters/<cluster>" \ | awk -F': ' '/^[Ee][Tt][Aa][Gg]:/ {gsub(/\r/,""); print $2}')
STATUS=$(curl -s -o /tmp/body -w "%{http_code}" -X PATCH \ -H "Authorization: Bearer $KUPE_API_KEY" \ -H "Content-Type: application/json" \ -H "If-Match: $ETAG" \ "https://api.kupe.cloud/api/v1/tenants/<tenant>/clusters/<cluster>" \ -d '{"version": "1.32"}')
if [ "$STATUS" = "200" ]; then echo "Updated" exit 0 elif [ "$STATUS" = "412" ]; then echo "Conflict, retrying (attempt $attempt)" continue else echo "Failed with $STATUS" >&2 cat /tmp/body >&2 exit 1 fidone
echo "Gave up after 3 attempts" >&2exit 1Retry strategy
Section titled “Retry strategy”A safe default for retryable status codes 429, 500, and 503:
- exponential backoff with jitter, starting at 1 second and capping at 30 seconds
- up to 5 attempts
- retry
GET,DELETE, andPATCHwhen you useIf-Match - treat
POSTmore carefully, because success may already have happened before the retry
If you hit 429 repeatedly, reduce request concurrency rather than only waiting longer.