Error-Envelope
v2-Error-Envelope (Standard seit quickfix-B) - vollständiges Schema, Fehlercodes, Request-IDs und v1-Legacy-Migration.
Jede Antwort der CodeCourier-API, die nicht 2xx ist, nutzt einen strukturierten JSON-Envelope. Clients können auf error.code branchen, um zuverlässig programmatisch zu reagieren, und error.message an Menschen ausgeben.
v2 ist der Standard seit quickfix-B. Aufrufer erhalten den strukturierten Envelope automatisch - ein Header-Opt-in ist nicht erforderlich. Das alte v1-Schema mit reinem String bleibt während des Deprecation-Fensters über den Request-Header X-API-Version: 1 verfügbar und wird am 2026-10-24 entfernt.
v2-Envelope (Standard)
Jede Fehlerantwort trägt das folgende Schema. Quelle der Wahrheit: convex/lib/apiResponse.ts (respondError).
{
"error": {
"code": "VALIDATION",
"message": "Invalid request arguments",
"field": "workflowId",
"requestId": "5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2",
"retryable": false
}
}code- stabiler, maschinenlesbarer Bezeichner aus derErrorCode-Union. Niemals übersetzt, niemals umbenannt.message- menschenlesbare Beschreibung auf Englisch. Sicher zur Anzeige; auf dem Client mitcodeals i18n-Schlüssel lokalisierbar.field- optional. BeiVALIDATION-Fehlern vorhanden, um das fehlerhafte Request-Feld zu kennzeichnen.requestId- serverseitig generierte UUID. Im Response-HeaderX-Request-IDbei jeder Antwort (Erfolg und Fehler) gespiegelt. Immer loggen; in Support-Tickets stets angeben.retryable- optional. Serverseitiger Hinweis, dass der Aufrufer mit Backoff erneut versuchen sollte. Standardmäßigtruebei 5xx, sonst nicht gesetzt.
Taxonomie der Fehlercodes
Die ErrorCode-Union wird aus convex/lib/apiResponse.ts exportiert. Codes sind in SCREAMING_SNAKE_CASE und werden über classifyError() auf HTTP-Statuscodes abgebildet.
400 - Client-Validierung
VALIDATION- Request-Body / Query / Pfad hat die Schema-Validierung nicht bestanden.fieldkennzeichnet den fehlerhaften Schlüssel.
401 - Authentifizierung
AUTH_MISSING- keinAuthorization-Header übergeben.AUTH_INVALID- Bearer-Token ist fehlerhaft, unbekannt oder widerrufen.
403 - Autorisierung
FORBIDDEN- Aufrufer ist authentifiziert, hat aber keinen Zugriff auf die Ressource (Projekt passt nicht, Mitgliedschaft fehlt usw.).SCOPE_DENIED- dem API-Key fehlt der erforderliche Scope. Scope im Dashboard hinzufügen und erneut versuchen.
404 - Nicht gefunden
NOT_FOUND- Ressource existiert nicht oder wurde gelöscht.
409 - Konflikt
CONFLICT- generischer optimistischer Concurrency-/Zustandskonflikt. Neu laden und erneut versuchen.IDEMPOTENCY_MISMATCH- derselbeIdempotency-Keymit anderem Body wiederverwendet. Siehe Idempotenz.IDEMPOTENCY_IN_PROGRESS- gleicher Key, ursprüngliche Anfrage läuft noch. Nach kurzer Verzögerung erneut versuchen.
500 / 501 - Server
INTERNAL- unerwarteter Fehler.requestIdim Support-Ticket angeben. Mitretryable: truemarkiert.NOT_IMPLEMENTED- Endpoint ist reserviert, aber noch nicht implementiert.
v1-Legacy-Envelope (Opt-out)
Clients auf dem Deprecation-Pfad können das alte Schema durch Senden des Headers X-API-Version: 1 anfordern. Der Body ist ein einfacher String:
// Request:
// GET /api/v1/workflows/get?id=missing
// Authorization: Bearer cc_live_...
// X-API-Version: 1
//
// Response:
// HTTP/2 404
// X-Request-ID: 5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2
{
"error": "Workflow 'missing' does not exist."
}v1-Antworten enthalten weiterhin den Header X-Request-ID (eingeführt in quickfix-A), damit Betreiber Fehler auch dann korrelieren können, wenn Clients noch nicht migriert sind.
Sunset: 2026-10-24. Nach diesem Datum hat der Header X-API-Version: 1 keine Wirkung mehr, und alle Aufrufer erhalten den v2-Envelope.
Migration
Wenn Ihr Client v1 erwartet, senden Sie den Request-Header X-API-Version: 1, bis Sie auf v2 umstellen können. Zur Migration:
- Entfernen Sie den Header
X-API-Version, damit der Server v2 standardmäßig liefert. - Ersetzen Sie String-Parsing durch strukturelles Parsing auf
error.code. - Geben Sie
error.requestId(oder den Response-HeaderX-Request-ID) in Ihren Logs und in der Fehleranzeige aus. - Berücksichtigen Sie
error.retryablein Retry-Policies. Werten Sie Abwesenheit für 4xx alsfalseund für 5xx alstrue.
Fehler behandeln
curl (v2-Standard)
curl -i https://<your-deployment>.convex.site/api/v1/runs/start \
-H "Authorization: Bearer cc_live_..." \
-H "Content-Type: application/json" \
-d '{"workflowId": "missing"}'
# HTTP/2 404
# X-Request-ID: 5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2
# {
# "error": {
# "code": "NOT_FOUND",
# "message": "Workflow 'missing' does not exist.",
# "requestId": "5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2"
# }
# }TypeScript
type ErrorCode =
| "AUTH_INVALID"
| "AUTH_MISSING"
| "FORBIDDEN"
| "SCOPE_DENIED"
| "NOT_FOUND"
| "VALIDATION"
| "CONFLICT"
| "INTERNAL"
| "NOT_IMPLEMENTED"
| "IDEMPOTENCY_MISMATCH"
| "IDEMPOTENCY_IN_PROGRESS";
type ApiError = {
error: {
code: ErrorCode;
message: string;
field?: string;
requestId: string;
retryable?: boolean;
};
};
async function call<T>(url: string, init: RequestInit): Promise<T> {
const res = await fetch(url, init);
if (res.ok) return (await res.json()) as T;
const body = (await res.json()) as ApiError;
switch (body.error.code) {
case "SCOPE_DENIED":
throw new Error(`Missing scope. requestId=${body.error.requestId}`);
case "VALIDATION":
throw new Error(`Bad field: ${body.error.field ?? "<unknown>"}`);
case "IDEMPOTENCY_MISMATCH":
throw new Error("Use a fresh Idempotency-Key.");
case "INTERNAL":
// body.error.retryable === true - retry with backoff.
throw new Error(`Server error. requestId=${body.error.requestId}`);
default:
throw new Error(`${body.error.code}: ${body.error.message}`);
}
}Python
import requests
class ApiError(Exception):
def __init__(self, code, message, request_id, field=None, retryable=None):
super().__init__(f"{code}: {message} [requestId={request_id}]")
self.code = code
self.request_id = request_id
self.field = field
self.retryable = retryable
def call(method, url, **kwargs):
r = requests.request(method, url, timeout=30, **kwargs)
if r.ok:
return r.json()
body = r.json().get("error", {})
raise ApiError(
code=body.get("code", "UNKNOWN"),
message=body.get("message", r.text),
request_id=body.get("requestId") or r.headers.get("X-Request-ID", "<none>"),
field=body.get("field"),
retryable=body.get("retryable"),
)Lokalisierung
Da error.code stabil ist, können Client-Apps übersetzte Nachrichten nach Code nachschlagen:
const messages = {
de: { SCOPE_DENIED: "Dem API-Schlüssel fehlt ein erforderlicher Scope." },
en: { SCOPE_DENIED: "API key is missing a required scope." },
};
const display = messages[userLocale][err.code] ?? err.message;Request-IDs
Der Header X-Request-ID wird bei jeder Antwort ausgegeben (Erfolg und Fehler, v1 und v2). Der v2-Body stellt denselben Wert zusätzlich als error.requestId (bzw. data.requestId bei erfolgreichen Antworten) bereit, damit Client-SDKs korrelieren können, ohne Header zu parsen. Loggen Sie die ID immer; geben Sie sie stets in Support-Tickets an.