Enveloppe d'erreur
Enveloppe d'erreur v2 (PAR DÉFAUT depuis quickfix-B) - schéma complet, codes d'erreur, request IDs et migration depuis la v1 legacy.
Chaque réponse non-2xx de l’API CodeCourier utilise une enveloppe JSON structurée. Les clients peuvent aiguiller sur error.code pour une gestion programmatique fiable et faire remonter error.message aux humains.
La v2 est le défaut depuis quickfix-B. Les appelants reçoivent automatiquement l’enveloppe structurée - aucun opt-in par header n’est requis. La forme legacy v1 en chaîne uniquement reste disponible pendant la fenêtre de dépréciation via le header de requête X-API-Version: 1 et son retrait est prévu pour le 2026-10-24.
Enveloppe v2 (défaut)
Chaque réponse d’erreur porte la forme suivante. Source de vérité : convex/lib/apiResponse.ts (respondError).
{
"error": {
"code": "VALIDATION",
"message": "Invalid request arguments",
"field": "workflowId",
"requestId": "5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2",
"retryable": false
}
}code- identifiant stable et lisible par machine issu de l’unionErrorCode. Jamais traduit, jamais renommé.message- description lisible par un humain, en anglais. Sûr à afficher ; sûr à localiser côté client en utilisantcodecomme clé i18n.field- optionnel. Présent sur les erreursVALIDATIONpour pointer le champ de requête fautif.requestId- UUID généré par le serveur. Renvoyé dans le header de réponseX-Request-IDsur chaque réponse (succès et erreur). Loggez-le toujours ; citez-le dans les tickets de support.retryable- optionnel. Indice côté serveur que l’appelant devrait réessayer avec backoff. Vauttruepar défaut pour les 5xx et est absent sinon.
Taxonomie des codes d’erreur
L’union ErrorCode est exportée depuis convex/lib/apiResponse.ts. Les codes sont en SCREAMING_SNAKE_CASE et mappés aux codes de statut HTTP par classifyError().
400 - Validation client
VALIDATION- le body / query / path de requête a échoué à la validation de schéma.fieldpointe la clé fautive.
401 - Authentification
AUTH_MISSING- aucun headerAuthorizationn’a été fourni.AUTH_INVALID- le bearer token est malformé, inconnu ou révoqué.
403 - Autorisation
FORBIDDEN- l’appelant est authentifié mais ne peut pas accéder à la ressource (mismatch de projet, appartenance manquante, etc.).SCOPE_DENIED- la clé API n’a pas le scope requis. Ajoutez le scope dans le dashboard, puis réessayez.
404 - Non trouvé
NOT_FOUND- la ressource n’existe pas ou a été supprimée.
409 - Conflit
CONFLICT- conflit générique de concurrence optimiste / d’état. Re-fetchez et réessayez.IDEMPOTENCY_MISMATCH- mêmeIdempotency-Keyréutilisée avec un body différent. Voir Idempotence.IDEMPOTENCY_IN_PROGRESS- même clé, la requête originale est encore en vol. Réessayez après un court délai.
500 / 501 - Serveur
INTERNAL- échec inattendu. CitezrequestIddans votre ticket de support. Marquéretryable: true.NOT_IMPLEMENTED- l’endpoint est réservé mais pas encore implémenté.
Enveloppe v1 legacy (opt-out)
Les clients sur le rail de dépréciation peuvent demander l’ancienne forme en envoyant le header X-API-Version: 1. Le body est une simple chaîne :
// 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."
}Les réponses v1 portent toujours le header X-Request-ID (ajouté dans quickfix-A), de sorte que les opérateurs peuvent corréler les échecs même lorsque les clients n’ont pas migré.
Sunset : 2026-10-24. Après cette date, le header X-API-Version: 1 devient un no-op et tous les appelants reçoivent l’enveloppe v2.
Migration
Si votre client attend la v1, envoyez le header X-API-Version: 1 jusqu’à ce que vous puissiez passer à la v2. Pour migrer :
- Retirez le header
X-API-Versionpour que le serveur émette la v2 par défaut. - Remplacez le parsing de chaîne par un parsing structurel sur
error.code. - Faites remonter
error.requestId(ou le header de réponseX-Request-ID) dans vos logs et votre UI d’erreur. - Honorez
error.retryabledans vos politiques de retry. Traitez l’absence commefalsepour les 4xx ettruepour les 5xx.
Gérer les erreurs
curl (v2 par défaut)
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"),
)Localisation
Parce que error.code est stable, les applications clientes peuvent rechercher des messages traduits indexés par code :
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
Le header X-Request-ID est émis sur chaque réponse (succès et erreur, v1 et v2). Le body v2 fait en outre remonter la même valeur en tant que error.requestId (ou data.requestId sur les réponses réussies), de sorte que les SDK clients peuvent corréler sans parser les headers. Loggez toujours l’ID ; incluez-le toujours dans les tickets de support.