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 deBearer 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.
validation_error422
Ce que ça veut dire. Le body du POST ne respecte
pas le schéma attendu par l'endpoint. Inclut aussi les champs inconnus :
les schémas de POST /v1/calls rejettent tout champ non déclaré
— un typo comme custommer au lieu de customer, ou
phonne_number au lieu de phone_number dans
customer, est refusé. 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 :
amountenvoyé en nombre au lieu de string. - Contrainte violée :
customer.namevide,amount ≤ 0. - Enum :
countryhorsMA/DZ/TN/FR.
Comment corriger
- Parser
errors[].loceterrors[].msgpour cibler le champ exact (loc peut avoir plusieurs segments pour les champs nested, ex.["body", "customer", "phone_number"]). - Consulter les schémas
CallCreateetCustomerInputdans 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é.
Distingué du 404 pour aider à diagnostiquer un bug de construction d'URL côté client.
Comment corriger
key_iddoit être un UUID v4 strict (format8-4-4-4-12).- Relire la valeur retournée par
POST /v1/me/api-keys(id).
invalid_period_format400
Ce que ça veut dire. GET /v1/me/usage?period=...
avec format non-YYYY-MM ou mois hors 01-12. 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
periodpour obtenir le mois courant UTC (défaut).
invalid_status_filter400
Ce que ça veut dire.
GET /v1/me/webhooks/deliveries?status=... avec valeur hors
delivered/failed/pending.
Comment corriger
- Utiliser exactement
delivered,failed, oupending. - Le
detailde la réponse liste les valeurs acceptées.
invalid_cursor400
Ce que ça veut dire. Curseur de pagination malformé,
avec signature invalide, ou clé de signature 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).
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.
missing_idempotency_key400
Ce que ça veut dire. Header Idempotency-Key
absent sur POST /v1/calls. Obligatoire 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.
idempotency_key_empty400
Ce que ça veut dire. Header Idempotency-Key
présent mais vide. Doit faire au moins 1 caractère.
idempotency_key_too_long400
Ce que ça veut dire. Header Idempotency-Key
> 255 caractères. Limite stricte serveur pour éviter un abus storage.
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-Keypour 2 requêtes sémantiquement différentes. - Un retry légitime (même body) renvoie la réponse d'origine, pas une erreur.
order_external_id_already_exists409
Ce que ça veut dire. Un POST /v1/calls
envoie une order.external_id déjà utilisée pour cette boutique.
Les commandes sont créées en insertion stricte, sans écrasement —
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_idsans coordination. - Collision involontaire (formats courts type
CMD-001).
Comment corriger
- Retenter sur la commande existante : nouveau POST avec nouvelle
Idempotency-Key+order.previous_attemptsincrémenté. - Créer une commande distincte : générer une
external_idunique 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 shop.",
"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
shop_not_found404
Ce que ça veut dire. Aucune boutique active ne porte cet identifiant sur votre compte. Le corps de la réponse est identique dans trois cas — la boutique n'existe pas, elle a été archivée, ou elle appartient à un autre compte. C'est volontaire : une réponse différenciée permettrait de deviner quelles boutiques existent chez les autres.
Causes fréquentes
- Faute de frappe dans l'identifiant, ou confusion entre
external_id(la boutique) etmerchant_external_id(son propriétaire). - Boutique archivée par un
DELETEprécédent — elle reste consultable viaGET /v1/shops?status=archived. - Boutique créée sur un autre compte, ou avec une autre clé API.
Comment corriger
- Vérifier l'identifiant avec
GET /v1/shops, qui liste les boutiques du compte. - Chercher parmi les archivées :
?status=all. - La créer si elle n'existe pas encore :
POST /v1/shops.
Exemple de réponse
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://docs.yasmine.akidly.com/errors/shop_not_found",
"title": "Not Found",
"status": 404,
"detail": "Aucune boutique active avec cet identifiant sur votre compte.",
"instance": "/v1/shops/shop-4471",
"request_id": "a1b2c3d4"
}
↑ Retour en haut
shop_external_id_already_exists409
Ce que ça veut dire. Une boutique existe déjà avec cet
external_id sur votre compte. La création est stricte : elle ne se
transforme jamais en mise à jour implicite. Créer deux fois la même boutique est
une erreur d'intégration, pas une modification.
Causes fréquentes
- Rejeu d'une création déjà passée — le premier appel a réussi.
- Tentative de modifier une boutique via
POSTau lieu dePATCH. - Collision d'identifiants entre deux systèmes qui alimentent le même compte.
Comment corriger
- Pour modifier la boutique existante :
PATCH /v1/shops/{external_id}. - Pour en créer une distincte : choisir un
external_idnon utilisé. - Rejeu après un timeout réseau : ce 409 confirme que la
création initiale a abouti. Un
GETrenvoie la fiche.
Exemple de réponse
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://docs.yasmine.akidly.com/errors/shop_external_id_already_exists",
"title": "Conflict",
"status": 409,
"detail": "Une boutique existe deja avec l'identifiant 'shop-4471' sur votre compte. Utiliser PATCH pour la modifier, ou un autre identifiant pour en creer une nouvelle.",
"instance": "/v1/shops",
"request_id": "a1b2c3d4"
}
↑ Retour en haut
product_not_found404
Ce que ça veut dire. Aucun produit actif ne porte cet identifiant dans cette boutique. Le corps est identique dans trois cas — le produit n'existe pas, il a été archivé, ou il appartient à une autre boutique. Une réponse différenciée permettrait de deviner ce qui existe ailleurs.
Causes fréquentes
- Produit déclaré dans une autre boutique du même compte : l'identifiant n'est unique qu'à l'intérieur d'une boutique.
- Produit archivé par un
DELETEprécédent — toujours visible via?status=archived. - Confusion entre l'identifiant du produit et celui d'une de ses variantes.
Comment corriger
- Lister le catalogue de la boutique :
GET /v1/shops/{shop_external_id}/products. - Chercher parmi les archivés :
?status=all. - Le créer s'il n'existe pas encore.
Exemple de réponse
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://docs.yasmine.akidly.com/errors/product_not_found",
"title": "Not Found",
"status": 404,
"detail": "Aucun produit actif avec cet identifiant dans cette boutique.",
"instance": "/v1/shops/shop-4471/products/TSHIRT-99",
"request_id": "a1b2c3d4"
}
↑ Retour en haut
product_external_id_already_exists409
Ce que ça veut dire. Un produit porte déjà cet
external_id dans cette boutique. La création est stricte : elle
ne se transforme jamais en mise à jour implicite.
Causes fréquentes
- Rejeu d'une création déjà passée — le premier appel a réussi.
- Tentative de modifier un produit via
POSTau lieu dePATCH. - Import en masse relancé après une interruption.
Comment corriger
- Pour modifier :
PATCH /v1/shops/{shop_external_id}/products/{product_external_id}. - Pour en créer un distinct : choisir un
external_idlibre. - Rejeu après un timeout réseau : ce 409 confirme que la
création initiale a abouti. Un
GETrenvoie la fiche.
Exemple de réponse
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://docs.yasmine.akidly.com/errors/product_external_id_already_exists",
"title": "Conflict",
"status": 409,
"detail": "Un produit existe deja avec l'identifiant 'TSHIRT-01' dans la boutique 'shop-4471'. Utiliser PATCH pour le modifier, ou un autre identifiant pour en creer un nouveau.",
"instance": "/v1/shops/shop-4471/products",
"request_id": "a1b2c3d4"
}
↑ Retour en haut
order_item_unknown_product422
Ce que ça veut dire. Une ligne de commande référence un
product_external_id — ou un variant_external_id —
qui n'existe pas dans la boutique de cet appel. L'appel n'a pas été créé,
rien n'a été facturé.
Causes fréquentes
- Produit déclaré dans une autre boutique : l'identifiant n'est unique qu'à l'intérieur d'une boutique.
- Produit archivé depuis, ou jamais déclaré.
- Commande envoyée avant que le catalogue ne soit synchronisé.
Comment corriger
- Déclarer le produit :
POST /v1/shops/{shop_external_id}/products. - Ou décrire l'article sur place : une ligne sans
product_external_idmais avecnameetunit_pricereste parfaitement valide. C'est la voie pour un article sur mesure, un frais de port, un cadeau — un produit non déclaré ne doit jamais bloquer un appel.
Exemple de réponse
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://docs.yasmine.akidly.com/errors/order_item_unknown_product",
"title": "Unprocessable Entity",
"status": 422,
"detail": "Le produit 'BOUGIE-99' n'existe pas dans la boutique 'shop-4471'.",
"instance": "/v1/calls",
"request_id": "a1b2c3d4"
}
↑ Retour en haut
order_item_variant_required422
Ce que ça veut dire. La ligne référence un produit qui a plusieurs déclinaisons — tailles, couleurs — sans préciser laquelle. Nous n'en choisissons jamais une au hasard : l'agent annoncerait au client un article qu'il n'a pas commandé.
Le detail liste les variantes existantes :
vous n'avez pas à aller les chercher.
Comment corriger
- Préciser
variant_external_idsur la ligne, à côté deproduct_external_id. - Vérifier le catalogue :
GET /v1/shops/{shop_external_id}/products/{product_external_id}renvoie toutes les variantes avec leur prix.
Un produit sans déclinaison n'est jamais concerné : sa variante unique est résolue automatiquement, et vous n'avez à parler que du produit.
Exemple de réponse
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://docs.yasmine.akidly.com/errors/order_item_variant_required",
"title": "Unprocessable Entity",
"status": 422,
"detail": "Le produit 'TSHIRT-01' a 2 variantes : TSH-S, TSH-XL. Preciser laquelle via `variant_external_id`.",
"instance": "/v1/calls",
"request_id": "a1b2c3d4"
}
↑ 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 compteur de rate limit (rare). En pratique vous verrez plutôt
rate_limit_exceeded.
rate_limit_exceeded429
Ce que ça veut dire. Rate-limit par clé API dépassé. 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/-Resetsur les 2xx pour prévenir la saturation. - Chaque 429 est enregistré comme event
rate_limitedsur la clé — visible dansGET /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).
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 (anti-énumération).
É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.
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 hautapi_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 . Body byte-identique — anti-énumération.
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.
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— pashttps(ouhttpen dev).localhost_rejected—localhost/127.0.0.1interdits.dns_resolution_failed— le hostname ne résout pas.private_ip_rejected— IP privée (RFC 1918) ou link-local interdite.
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/webhooksd'abord pour rotate.- Le nouveau POST retournera un secret HMAC frais.
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.
webhook_url_not_configured400
Ce que ça veut dire.
POST /v1/me/webhooks/test sans webhook actif. Impossible de
tester une URL qui n'existe pas.
Comment corriger
- Configurer un webhook via
POST /v1/me/webhooksavant de tester.
payload_too_large413
Ce que ça veut dire. Émis sur POST /webhooks/whatsapp
quand le body dépasse 256 KiB. 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 coupe plus tôt). - Si vu côté Meta retry storm : contacter le support avec
request_id.
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.
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
languagedu payload — la langue locale du pays sera appliquée par défaut (MA/DZ/TN→ar,FR→fr). - Soit fournir une combinaison valide (
FR+fr, ou un pays maghrébin avecar/fr).
channel_not_available422
Ce que ça veut dire. POST /v1/calls demande un
call_params.channel qui n'est pas ouvert sur cette installation. Le
detail nomme les canaux disponibles ainsi que le canal par défaut.
Le refus est volontairement explicite : un appel discrètement re-routé vers un autre canal que celui demandé serait indétectable de votre côté. Mieux vaut une erreur claire qu'un comportement silencieux.
Exemple de réponse
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://docs.yasmine.akidly.com/errors/channel_not_available",
"title": "Unprocessable Entity",
"status": 422,
"detail": "Le canal `whatsapp` n'est pas ouvert sur cette installation. Canaux disponibles : gsm. Omettre `call_params.channel` sert le canal par defaut (`gsm`).",
"instance": "/v1/calls",
"request_id": "a1b2c3d4"
}
Remédiation
- Retirer
call_params.channeldu payload — c'est le cas normal. Le canal par défaut s'applique, et il convient dans la quasi-totalité des intégrations. - Ou demander un canal nommé comme disponible dans le
detail.
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_idet contacter le support si vous voyez ce slug. - Côté SDK reseller : router sur le code HTTP en fallback quand
typetermine par/http_error.