Erreurs API Yasmine

Format RFC 7807 sur tout /v1/*. Chaque classe d'erreur a un type stable ; router dessus côté SDK, pas sur le code HTTP seul. Format URL : https://docs.yasmine.akidly.com/errors/<slug> → redirige vers la section ci-dessous.

invalid_api_key401

Ce que ça veut dire. Header Authorization absent, mal formé (pas de Bearer), ou clé invalide/révoquée. Le header WWW-Authenticate: Bearer est propagé.

Causes fréquentes

  • Clé copiée avec espace/newline en fin (le serveur hash la clé entière).
  • Clé révoquée via DELETE /v1/me/api-keys/{id}.
  • Bearer prefix manquant : Authorization: yk_... au lieu de Bearer yk_....

Comment corriger

  • Régénérer une clé via POST /v1/me/api-keys.
  • Vérifier l'état de vos clés : GET /v1/me/api-keys.
  • Consulter les events d'audit de la clé : GET /v1/me/api-keys/{id}/events.

Exemple de réponse

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Content-Type: application/problem+json

{
  "type": "https://docs.yasmine.akidly.com/errors/invalid_api_key",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authorization: Bearer <reseller_api_key> requis",
  "instance": "/v1/calls",
  "request_id": "a1b2c3d4"
}
↑ Retour en haut

forbidden403

Ce que ça veut dire. Clé valide, mais sans droit sur cette ressource. Aucun endpoint /v1/* ne l'émet aujourd'hui — slug réservé pour une future séparation admin/reader.

Comment corriger

  • Contacter le support si vous êtes admin et voyez ce code.
↑ Retour en haut

validation_error422

Ce que ça veut dire. Le body du POST ne respecte pas le schéma Pydantic du endpoint. Inclut aussi les champs inconnus : extra="forbid" sur CallCreate et CustomerInput — un typo comme custommer au lieu de customer, ou phonne_number au lieu de phone_number dans customer, est refusé (P1-12). Le champ errors[] liste chaque violation avec son chemin exact.

Causes fréquentes

  • Typo sur le nom d'un champ (errors[].type === "extra_forbidden").
  • Type mauvais : amount envoyé en nombre au lieu de string.
  • Contrainte violée : customer.name vide, amount ≤ 0.
  • Enum : country hors MA/DZ/TN/FR.

Comment corriger

  • Parser errors[].loc et errors[].msg pour cibler le champ exact (loc peut avoir plusieurs segments pour les champs nested, ex. ["body", "customer", "phone_number"]).
  • Consulter les schémas CallCreate et CustomerInput dans la spec OpenAPI.
  • Pour un champ en trop : vérifier l'orthographe ou supprimer du body.

Exemple de réponse

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://docs.yasmine.akidly.com/errors/validation_error",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Un ou plusieurs champs du payload sont invalides.",
  "instance": "/v1/calls",
  "request_id": "a1b2c3d4",
  "errors": [
    { "loc": ["body", "customer", "phonne_number"], "msg": "Extra inputs are not permitted", "type": "extra_forbidden" },
    { "loc": ["body", "amount"], "msg": "Decimal input should be greater than 0", "type": "greater_than" }
  ]
}
↑ Retour en haut

invalid_key_id400

Ce que ça veut dire. DELETE /v1/me/api-keys/{key_id} ou GET /v1/me/api-keys/{key_id}/events avec UUID malformé (P1-4/P1-6). Distingué du 404 pour aider à diagnostiquer un bug de construction d'URL côté client.

Comment corriger

  • key_id doit être un UUID v4 strict (format 8-4-4-4-12).
  • Relire la valeur retournée par POST /v1/me/api-keys (id).
↑ Retour en haut

invalid_period_format400

Ce que ça veut dire. GET /v1/me/usage?period=... avec format non-YYYY-MM ou mois hors 01-12 (P1-1). Exemples de valeurs rejetées : ?period=abc, ?period=2026-13, ?period=2026-00.

Comment corriger

  • Format strict ^\d{4}-(0[1-9]|1[0-2])$.
  • Omettre period pour obtenir le mois courant UTC (défaut).
↑ Retour en haut

invalid_status_filter400

Ce que ça veut dire. GET /v1/me/webhooks/deliveries?status=... avec valeur hors delivered/failed/pending (P1-2).

Comment corriger

  • Utiliser exactement delivered, failed, ou pending.
  • Le detail de la réponse liste les valeurs acceptées.
↑ Retour en haut

invalid_cursor400

Ce que ça veut dire. Curseur de pagination (P1-3) malformé, avec signature tampered, ou CURSOR_SIGNING_KEY tournée côté serveur. S'applique à tous les endpoints paginés : GET /v1/calls, GET /v1/me/transactions, GET /v1/me/webhooks/deliveries, GET /v1/me/api-keys/{id}/events.

Comment corriger

  • Refaire la 1re requête sans le paramètre cursor.
  • Ne pas stocker de curseurs en durable (TTL 24 h + rotation possible de la clé de signature).
↑ Retour en haut

cursor_expired400

Ce que ça veut dire. Curseur plus vieux que 24 h (TTL fixe). Yasmine ne propose pas de curseur « temps-indépendant » pour limiter la surface en cas de fuite de clé.

Comment corriger

  • Refaire la 1re requête sans cursor.
  • Reprendre la pagination depuis le début avec la page 1 fraîche.
↑ Retour en haut

missing_idempotency_key400

Ce que ça veut dire. Header Idempotency-Key absent sur POST /v1/calls. Obligatoire depuis P0-1 pour éviter un double appel en cas de retry réseau.

Comment corriger

  • Générer un UUID v4 par requête (uuidgen, crypto.randomUUID(), etc.).
  • Même valeur dans un retry automatique → la réponse cachée est servie (pas de double appel).
  • Fenêtre de dedup : 24 h.
↑ Retour en haut

idempotency_key_empty400

Ce que ça veut dire. Header Idempotency-Key présent mais vide. Doit faire au moins 1 caractère.

↑ Retour en haut

idempotency_key_too_long400

Ce que ça veut dire. Header Idempotency-Key > 255 caractères. Limite stricte serveur pour éviter un abus storage.

↑ Retour en haut

idempotency_key_conflict409

Ce que ça veut dire. La même Idempotency-Key a déjà été utilisée dans la fenêtre 24 h avec un body différent. Générer une nouvelle clé pour une nouvelle requête. Le body original n'est jamais ré-exposé (anti-BOPLA).

Comment corriger

  • Ne jamais réutiliser une Idempotency-Key pour 2 requêtes sémantiquement différentes.
  • Un retry légitime (même body) renvoie la réponse d'origine, pas une erreur.
↑ Retour en haut

order_external_id_already_exists409

Ce que ça veut dire. Un POST /v1/calls envoie une order.external_id déjà utilisée pour ce merchant. Les commandes sont créées par INSERT strict (pas d'upsert) depuis Phase 2-final — un second POST avec la même external_id est rejeté.

Causes fréquentes

  • Retry d'une commande sans réutiliser la même Idempotency-Key (qui aurait fait un replay).
  • Deux services du reseller qui partagent le même pool d'external_id sans coordination.
  • Collision involontaire (formats courts type CMD-001).

Comment corriger

  • Retenter sur la commande existante : nouveau POST avec nouvelle Idempotency-Key + order.previous_attempts incrémenté.
  • Créer une commande distincte : générer une external_id unique côté reseller.
  • Retry réseau pur : garder la même Idempotency-Key → replay bit-for-bit.

Exemple de réponse

HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://docs.yasmine.akidly.com/errors/order_external_id_already_exists",
  "title": "Order external_id already exists",
  "status": 409,
  "detail": "An order with this external_id already exists for this merchant.",
  "instance": "/v1/calls",
  "request_id": "a1b2c3d4",
  "hint": "To retry reaching the customer on this existing order, submit a new POST with a fresh Idempotency-Key and increment order.previous_attempts. To create a distinct order, use a unique external_id."
}
↑ Retour en haut

insufficient_balance402

Ce que ça veut dire. Solde strictement inférieur au minimum facturable (10 secondes) pour initier un appel. L'appel n'a pas été lancé.

Causes fréquentes

  • Recharge automatique non configurée ou échouée.
  • Consommation sur la période supérieure à l'attendu.
  • Clé API partagée dev/prod qui consomme le même crédit.

Comment corriger

  • Lire le solde : GET /v1/me/balance.
  • Vérifier la consommation : GET /v1/me/usage?period=YYYY-MM.
  • Recharger via le portail ou l'API facturation.

Exemple de réponse

HTTP/1.1 402 Payment Required
Content-Type: application/problem+json

{
  "type": "https://docs.yasmine.akidly.com/errors/insufficient_balance",
  "title": "Payment Required",
  "status": 402,
  "detail": "Solde disponible 3s < minimum facturable 10s.",
  "instance": "/v1/calls",
  "request_id": "a1b2c3d4",
  "balance_seconds": 3,
  "required_seconds": 10
}
↑ Retour en haut

rate_limited429

Ce que ça veut dire. Slug fallback générique émis si un 429 est levé hors du handler slowapi (rare). En pratique vous verrez plutôt rate_limit_exceeded.

↑ Retour en haut

rate_limit_exceeded429

Ce que ça veut dire. Rate-limit par clé API dépassé (M3.6 C7). Le bucket est remis à zéro en glissant à la fin de la fenêtre.

Seuils actuels

  • POST /v1/calls : 60 / min / clé.
  • POST /v1/calls/{id}/cancel : 120 / min.
  • GET /v1/* (lectures) : 600 / min.
  • POST /v1/me/webhooks* : 10 / min.
  • POST /v1/me/api-keys : 5 / min.

Comment corriger

  • Respecter le header Retry-After (secondes) avant de retenter.
  • Exploiter X-RateLimit-Limit / -Remaining / -Reset sur les 2xx pour prévenir la saturation.
  • Chaque 429 est enregistré comme event rate_limited sur la clé (P1-6) — visible dans GET /v1/me/api-keys/{id}/events.

Exemple de réponse

HTTP/1.1 429 Too Many Requests
Retry-After: 38
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1745229338
Content-Type: application/problem+json

{
  "type": "https://docs.yasmine.akidly.com/errors/rate_limit_exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Limite de frequence depassee (60 per 1 minute). Reessayer dans 38 seconde(s).",
  "instance": "/v1/calls",
  "request_id": "a1b2c3d4",
  "retry_after": 38
}
↑ Retour en haut

not_found404

Ce que ça veut dire. Ressource absente ou non accessible au reseller courant. Slug générique — certains endpoints utilisent un slug dédié (call_not_found, api_key_not_found, webhook_not_configured).

↑ Retour en haut

call_not_found404

Ce que ça veut dire. GET /v1/calls/{id}, GET /v1/calls/{id}/recording ou POST /v1/calls/{id}/cancel sur un call_id inexistant OU appartenant à un autre reseller (M3.6 C8, pattern anti-énum P0-2). Émis aussi par l'endpoint recording quand l'audio n'a pas (encore) été produit (call jamais finalisé). Body byte-identique dans tous les cas — pas de leak d'existence.

↑ Retour en haut

recording_gone410

Ce que ça veut dire. GET /v1/calls/{id}/recording sur un appel dont l'enregistrement audio a été purgé après la fenêtre de rétention (30 jours).

Pourquoi distinct de call_not_found

Pour permettre au reseller de différencier « jamais existé » (404) vs « a existé mais purgé après rétention » (410). Si vous stockez les recording_url côté reseller pour permettre aux opérateurs d'écouter les appels passés, traitez le 410 comme le signal de mettre à jour votre indexation locale (l'audio n'est plus disponible).

Correctif

Aucun retry possible — l'audio est définitivement supprimé. Si vous avez besoin d'archiver les enregistrements au-delà de 30 jours, téléchargez-les côté reseller et stockez-les chez vous.

↑ Retour en haut

api_key_not_found404

Ce que ça veut dire. DELETE /v1/me/api-keys/{id} ou GET /v1/me/api-keys/{id}/events sur une clé inexistante OU appartenant à un autre reseller (P1-4, P1-6). Body byte-identique — anti-énum P0-2.

↑ Retour en haut

conflict409

Ce que ça veut dire. Slug générique 409. Les cas concrets utilisent un slug dédié : idempotency_key_conflict, webhook_already_configured.

↑ Retour en haut

webhook_url_rejected400

Ce que ça veut dire. POST /v1/me/webhooks avec URL refusée par le guard SSRF. Le body contient un champ reason explicite.

Valeurs possibles de reason

  • invalid_url — URL non parseable.
  • scheme_not_allowed — pas https (ou http en dev).
  • localhost_rejectedlocalhost/127.0.0.1 interdits.
  • dns_resolution_failed — le hostname ne résout pas.
  • private_ip_rejected — IP privée (RFC 1918) ou link-local interdite.
↑ Retour en haut

webhook_already_configured409

Ce que ça veut dire. POST /v1/me/webhooks sur un reseller qui a déjà un webhook actif. 1 webhook max par reseller.

Comment corriger

  • DELETE /v1/me/webhooks d'abord pour rotate.
  • Le nouveau POST retournera un secret HMAC frais.
↑ Retour en haut

webhook_not_configured404

Ce que ça veut dire. GET /v1/me/webhooks ou DELETE /v1/me/webhooks sans configuration active. Pas de webhook à lire/supprimer.

↑ Retour en haut

webhook_url_not_configured400

Ce que ça veut dire. POST /v1/me/webhooks/test sans webhook actif (P1-2). Impossible de tester une URL qui n'existe pas.

Comment corriger

  • Configurer un webhook via POST /v1/me/webhooks avant de tester.
↑ Retour en haut

payload_too_large413

Ce que ça veut dire. Émis sur POST /webhooks/whatsapp quand le body dépasse 256 KiB (P0-6). Vérification avant HMAC (défense en profondeur contre DoS et payloads falsifiés).

Contexte

  • Meta envoie ~50 KB en pratique — marge 5×.
  • Pas applicable aux endpoints /v1/* (leur validation Pydantic coupe plus tôt).
  • Si vu côté Meta retry storm : contacter le support avec request_id.
↑ Retour en haut

bad_request400

Ce que ça veut dire. Slug fallback 400 générique. Les cas concrets utilisent un slug dédié (idempotency, cursor, period, key_id, webhook, validation). Un bad_request brut sans contexte indique un chemin d'erreur rare — contacter le support avec request_id.

↑ Retour en haut

service_unavailable503

Ce que ça veut dire. Dépendance externe indisponible ou config manquante côté Yasmine. Exemple concret : CURSOR_SIGNING_KEY non configurée → tous les endpoints paginés répondent 503 fail-closed plutôt que d'émettre des curseurs non signés.

Comment corriger (côté client)

  • Retenter après quelques secondes (backoff exponentiel recommandé).
  • Si persistant : contacter le support avec request_id.
↑ Retour en haut

internal_error500

Ce que ça veut dire. Erreur inattendue côté Yasmine (bug). La stack trace est loguée côté serveur avec le request_id — l'envoyer au support pour qu'il retrouve la trace exacte.

Exemple de réponse

HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json

{
  "type": "https://docs.yasmine.akidly.com/errors/internal_error",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "Une erreur interne est survenue. Contacter le support avec la valeur de request_id.",
  "instance": "/v1/calls",
  "request_id": "a1b2c3d4"
}
↑ Retour en haut

language_not_supported_for_country422

Ce que ça veut dire. POST /v1/calls avec une combinaison (country, language) non disponible. Aujourd'hui, seule la combinaison country=FR + language=ar est rejetée. Les autres combinaisons (MA/DZ/TN avec ar ou fr, FR avec fr) sont valides.

Exemple de réponse

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://docs.yasmine.akidly.com/errors/language_not_supported_for_country",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "La langue 'ar' n'est pas disponible pour le pays 'FR'. Combinaisons supportees : MA/DZ/TN avec `ar` ou `fr`, FR uniquement avec `fr`.",
  "instance": "/v1/calls",
  "request_id": "a1b2c3d4"
}

Remédiation

  • Soit retirer le champ language du payload — la langue locale du pays sera appliquée par défaut (MA/DZ/TNar, FRfr).
  • Soit fournir une combinaison valide (FR + fr, ou un pays maghrébin avec ar/fr).
↑ Retour en haut

http_error4xx/5xx

Ce que ça veut dire. Slug fallback générique pour les codes HTTP non mappés explicitement (cas marginal). Le code HTTP réel est toujours dans le champ status du body. Ne pas dépendre de ce slug dans votre code — un code HTTP donné finira toujours par avoir un slug stable dédié.

Remédiation

  • Logger le request_id et contacter le support si vous voyez ce slug.
  • Côté SDK reseller : router sur le code HTTP en fallback quand type termine par /http_error.
↑ Retour en haut