Journaux SMS
Répondez à une question : qu'est-il arrivé à cet SMS livré par Clerk ? Les Application Logs sms.* enregistrent le cycle de vie de la livraison de chaque SMS que Clerk envoie au nom d'une instance — codes de vérification téléphonique, OTPs, codes de réinitialisation de mot de passe — pour que vous puissiez distinguer une inscription bloquée d'un rejet de l'opérateur d'un blocage côté Clerk sans deviner.
Lisez-les de deux façons : dans le Clerk Dashboard → Application Logs (filtrer les types d'événement sms.*), ou par programmation via la Backend API (GET /v1/logs) — les mêmes données, scriptables pour une investigation reproductible. Il s'agit de télémétrie de livraison sur l'activité des utilisateurs finaux, non sur les actions du tableau de bord.
Interroger les journaux
Les endpoints logs de la Backend API prennent une clé secrète (sk_*). L'appelant le plus simple est le Clerk CLI, qui injecte l'authentification pour vous :
# Événements du cycle de vie SMS des dernières 24h, les plus récents en premier
SINCE=$(( ($(date +%s) - 86400) * 1000 ))
clerk api "/logs?type=sms.*&event_time_after=${SINCE}&limit=20"
# Uniquement les échecs, même fenêtre
clerk api "/logs?type=sms.failed&event_time_after=${SINCE}&limit=20"
# Historique SMS d'un numéro de téléphone (filtre de payload correspondance exacte)
# channel=sms exclut WhatsApp de la chronologie — voir la note ci-dessous
clerk api "/logs?type=sms.*&payload_filter[phone_number]=%2B14155550100&payload_filter[channel]=sms&event_time_after=${SINCE}"
# Pourquoi les envois d'un numéro ont échoué — le type concret déverrouille reason/raw_error
clerk api "/logs?type=sms.failed&payload_filter[phone_number]=%2B14155550100&event_time_after=${SINCE}&payload_fields=phone_number,reason,raw_error,rejected_before_send"
# Chaque événement d'un message, par sa trace
clerk api "/logs?type=sms.*&trace_id=<trace_id>&event_time_after=${SINCE}"
Limitez toujours la plage temporelle. Ces journaux sont à haut volume, commencez donc par un event_time_after étroit (et event_time_before quand vous savez à peu près quand l'SMS a été envoyé) et élargissez seulement si vous ne trouvez rien — ne scannez pas toute la fenêtre de rétention pour trouver un message. Gardez limit modeste (10–50) et paginez avec le curseur retourné plutôt que de l'augmenter. L'endpoint interroge dans des fenêtres de temps adaptatives, donc une page courte ou vide dans votre plage est normale : suivez starting_after jusqu'à ce que la réponse signale aucune page suivante.
*WhatsApp emprunte la même famille `sms.** (un pipeline ; le champ de payloadchannelestsmsouwhatsapp), donc une chronologie de numéro de téléphone peut mélanger les deux transports. Ce guide couvre le SMS — ajoutezpayload_filter[channel]=smspour le limiter à celui-ci, ou lisez le champchannel` pour les distinguer.
Ou appelez directement la Backend API :
SINCE=$(( ($(date +%s) - 86400) * 1000 ))
curl -s "https://api.clerk.com/v1/logs?type=sms.failed&event_time_after=${SINCE}&limit=20" \
-H "Authorization: Bearer $CLERK_SECRET_KEY" \
| python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin), indent=2))"
Paramètres de requête clés (tous optionnels sauf où une étape du guide en a besoin) :
| Param | Objectif |
|---|---|
type |
Type d'événement : concret (sms.failed) ou avec wildcard final (sms.*). Requis pour utiliser payload_filter. |
payload_filter[<field>] |
Correspondance exacte sur un champ de payload, par ex. payload_filter[user_id]=user_123. Encodez les valeurs en URL (un + dans un numéro E.164 devient %2B). |
trace_id |
Correler chaque événement d'un message. |
event_time_after / event_time_before |
Limites Unix en ms. Définissez au moins event_time_after à chaque requête — voir la note ci-dessus. |
limit, starting_after, ending_before |
Pagination par curseur. |
La réponse de liste omet le payload décodé par défaut ; ajoutez payload_fields (chemins de feuille séparés par des virgules) pour inclure des champs spécifiques, ou récupérez une ligne complète avec GET /v1/logs/{event_time_ms}:{event_id}.
reason, raw_error, et rejected_before_send n'existent que sur les événements d'échec, ils ne sont donc sélectionnables (et filtrables) que quand type est le concret sms.failed ou sms.undeliverable. Sous le wildcard sms.*, payload_fields et payload_filter sont validés par rapport à l'intersection de tous les cinq schémas — les champs communs (phone_number, user_id, phone_number_id, slug, verification_id, source_type, purpose, channel) — et demander reason là-bas est un 422. Utilisez sms.* pour voir la chronologie complète d'un numéro ; basculez vers type=sms.failed pour lire pourquoi il a échoué. Découvrez les champs exacts autorisés pour n'importe quel type avec GET /v1/logs/schemas?type=sms.failed (ou ?type=sms.* pour l'intersection).
Ces endpoints
logssont plus récents et nécessitent la fonctionnalité activée sur l'instance ; si un appel retourne 404 ou rien, confirmez que les journaux SMS sont activés pour l'instance avant de traiter un résultat vide comme « aucun tel SMS ».
Les cinq événements du cycle de vie
Un SMS traverse cette famille. Chaque événement est émis une fois, quand l'état mappé change pour la première fois — donc une livraison rapide peut passer directement à sms.delivered sans intermédiaire visible.
| Événement | Signification | Terminal ? |
|---|---|---|
sms.accepted |
Le fournisseur de livraison a accepté la transmission. Pas une preuve qu'il a atteint le téléphone. | Non |
sms.delivered |
L'opérateur a confirmé la livraison au téléphone. | Oui (succès) |
sms.failed |
L'envoi s'est arrêté avant la transmission, ou le fournisseur l'a refusé catégoriquement. Porte un reason. |
Oui (échec) |
sms.undeliverable |
L'opérateur a signalé qu'il ne pouvait pas livrer après que le fournisseur l'ait accepté. Porte un reason. |
Oui (échec) |
sms.unconfirmed |
Le fournisseur a explicitement signalé un résultat inconnu/non confirmé. Pas de preuve de livraison dans un sens ou l'autre. | Non — un delivered/undeliverable ultérieur peut encore le supplanter |
Le modèle mental : accepted → delivered est le chemin heureux. failed signifie qu'il n'est jamais parti (ou a été refusé à la porte) ; undeliverable signifie qu'il est parti mais l'opérateur l'a rejeté ; unconfirmed signifie que personne ne sait.
Guide de débogage
"L'utilisateur n'a jamais reçu le code." Trouvez le message — interrogez sms.* filtré par le numéro de téléphone ou l'ID utilisateur (payload_filter[phone_number] / payload_filter[user_id], plus payload_filter[channel]=sms pour garder WhatsApp dehors, selon la section ci-dessus). Ensuite, lisez l'événement le plus récent pour ce message :
- *Aucun événement `sms.` du tout** → les journaux n'établissent pas qu'un envoi a été tenté. Excluez d'abord une lacune dans la requête : confirmez que les journaux SMS sont activés pour l'instance, que vos limites temporelles couvrent l'envoi, et que vous avez paginé le curseur jusqu'à la fin (ces journaux sont au mieux un effort — voir Portée). Une fois que les journaux sont fiables et toujours vides, la cause est généralement en amont : la vérification n'a pas été créée, ou le numéro a été bloqué avant le pipeline SMS (vérifiez les événements Protect / détection de bot). Ce n'est pas un problème de livraison SMS.
sms.failed→ lisez lereason. Sirejected_before_sendest présent, Clerk l'a arrêté (blocage pays, limite mensuelle, limite de débit) — le correctif est sur votre configuration, pas l'opérateur. Sinon, le fournisseur l'a refusé ; voir le tableau des raisons.sms.acceptedmais pas desms.delivered→ il a quitté Clerk et l'opérateur n'a jamais confirmé. Attendez un moment (les accusés de livraison traînent), puis traitez une lacune durable comme un problème de l'opérateur/combiné pour ce numéro.sms.undeliverable→ l'opérateur l'a rejeté après l'avoir accepté. Lisez lereason;destination_unreachable/invalid_phone_numberpointent vers le numéro lui-même.sms.unconfirmed→ aucun signal de livraison n'existe encore. Ce n'est pas terminal : unsms.deliveredousms.undeliverableultérieur peut encore le supplanter, donc re-interrogez la trace avant de conclure. N'inférez pas le succès ou l'échec ; si un événement définitif n'est pas arrivé et l'utilisateur ne l'a pas reçu, faites-lui réessayer.
"Le SMS vers <country> est cassé." Cherchez sms.failed avec reason = country_not_supported (Clerk bloque le pays) ou restricted_destination (le fournisseur ne l'envoie pas là-bas). Un pic de reason = rate_limited avec rejected_before_send défini est le propre étranglement par numéro/préfixe de Clerk — attendu sous une attaque par pompage, pas une panne.
Lire un échec
sms.failed et sms.undeliverable portent un reason normalisé d'un ensemble fixe (le contrat stable et filtrable) plus un optionnel raw_error (les propres paroles du fournisseur, quand il en a signalé un — utile pour un cas spécifique, mais spécifique au fournisseur et instable, donc ne filtrez jamais dessus).
Le vocabulaire complet des raisons et ce que chaque valeur signifie — y compris quelles raisons n'apparaissent que sur les rejets côté Clerk avant envoi — est dans references/events.md.
Corréler le cycle de vie d'un message
Chaque événement pour un seul SMS partage le même trace. Quand un message a plusieurs événements (par ex. accepted puis undeliverable), ils se corrèlent sous un ID de trace même si leurs sujets peuvent différer. Utilisez la trace pour assembler la chronologie d'un message plutôt que de lire les événements isolément.
Portée et garanties
- SMS livrés par Clerk uniquement. Si vous apportez votre propre fournisseur SMS (
delivered_by_clerk = false), ces événements ne décrivent pas vos envois. - Les payloads ne contiennent jamais le corps du message, le code de vérification, ou l'identité du fournisseur. Déboguez à partir de
reason, horodatages, et les IDs de téléphone/utilisateur — pas des contenus de message qui ne sont pas là. - Télémétrie au mieux. Comme tous les Application Logs, la livraison de ces événements n'est pas garantie ; un événement manquant est une faible preuve. Ne construisez pas de logique application qui dépend de chaque événement SMS arrivant — pour l'état de livraison dont votre app doit agir, utilisez le propre statut de la vérification.
protect.sms.*(les blocages anti-abus de Clerk Protect) est une famille séparée ; un blocage côté Clerk s'affiche là, pas commesms.failed.
Référence complète événement/raison/payload : references/events.md.