Guide
Errors
Errors use HTTP status codes and an RFC 9457 problem body (Content-Type: application/problem+json).
{
"type": "https://cloudraw.com/problems/person-not-found",
"code": "person-not-found",
"title": "person not found",
"status": 404,
"detail": null
}| Field | Use |
|---|---|
code | Stable, machine-readable. Switch on this. A code is never renamed within v0. |
type | The same code as a URI. |
title, detail | For people (logs, error messages). Wording may change; do not parse it. |
errors | On validation errors: [{ "path": "email", "message": "…" }] per field, when available. |
retry_after_s | On 429 and 503: seconds to wait (also in the Retry-After header). |
Status codes
| Status | Meaning | Retry? |
|---|---|---|
| 200, 201 | Done; the body is the result. | — |
| 202 | Accepted. The body is a task: state: "done" when the change already happened. | — |
| 204 | Done, no body. | — |
| 400 | The request is invalid (see code and errors). | No — fix the request. |
| 401 | Missing, unknown, revoked or expired token. | No. |
| 403 | The token may not do this (scope, role ceiling, console-only). | No. |
| 404 | Not found. An id from another workspace also answers 404. | No. |
| 409 | Conflict with the current state (e.g. version-conflict, idempotency-key-reuse, a feature not set up yet). | After re-reading the state. |
| 429 | Rate limited. | Yes, after Retry-After. |
| 500, 502 | Our fault, or a system we depend on failed. | Yes, with backoff (and an Idempotency-Key). |
| 503 | busy: Cloudraw is switching servers or cannot save right now. | Yes, after Retry-After (usually 2–10 s). |
A retry helper
import time, uuid, requests
def call(s, method, url, **kw):
"""Retry 429 / 503 / network errors; one Idempotency-Key for every attempt of a POST."""
if method == "POST":
kw.setdefault("headers", {})["Idempotency-Key"] = str(uuid.uuid4())
for attempt in range(5):
try:
r = s.request(method, url, timeout=30, **kw)
except requests.ConnectionError:
time.sleep(2 ** attempt); continue
if r.status_code in (429, 503) or r.status_code >= 500:
time.sleep(int(r.headers.get("Retry-After", 2 ** attempt))); continue
if r.status_code >= 400:
p = r.json()
raise RuntimeError(f"{p['status']} {p['code']}: {p.get('detail')}")
return r.json() if r.content else None
raise RuntimeError("gave up after 5 attempts")export async function call(method, url, body) {
const headers = { Authorization: `Bearer ${process.env.CLOUDRAW_TOKEN}` };
if (method === "POST") headers["Idempotency-Key"] = crypto.randomUUID(); // same key for every attempt
if (body !== undefined) headers["Content-Type"] = "application/json";
for (let attempt = 0; attempt < 5; attempt++) {
let res;
try { res = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) }); }
catch { await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)); continue; }
if (res.status === 429 || res.status >= 500) {
await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") || 2 ** attempt) * 1000));
continue;
}
if (!res.ok) { const p = await res.json(); throw new Error(`${p.status} ${p.code}: ${p.detail ?? ""}`); }
return res.status === 204 ? null : res.json();
}
throw new Error("gave up after 5 attempts");
}function Invoke-Cloudraw($Method, $Uri, $Body) {
$h = @{ Authorization = "Bearer $env:CLOUDRAW_TOKEN" }
if ($Method -eq 'Post') { $h['Idempotency-Key'] = [guid]::NewGuid().ToString() }
for ($i = 0; $i -lt 5; $i++) {
try {
if ($Body) { return Invoke-RestMethod -Method $Method -Uri $Uri -Headers $h -ContentType 'application/json' -Body ($Body | ConvertTo-Json -Depth 10) }
return Invoke-RestMethod -Method $Method -Uri $Uri -Headers $h
} catch {
$r = $_.Exception.Response
if ($r -and ([int]$r.StatusCode -eq 429 -or [int]$r.StatusCode -ge 500)) {
$wait = 2 * ($i + 1); if ($r.Headers['Retry-After']) { $wait = [int]$r.Headers['Retry-After'] }
Start-Sleep -Seconds $wait; continue
}
throw
}
}
}