Aller au contenu

Suivi et conversions

Ce qu'un lien mesure vraiment, où trouver ces chiffres, et comment il se ferme.

L'API vous dit si un lien est toujours ouvert ; seul le Dashboard montre ses performances.

L'API n'a pas de compteurs

GET /links/{id} renvoie ce que le lien est, jamais ses performances : ni vues, ni conversions, ni revenus.

GEThttps://api.wajub.com/links/{id}
curl https://api.wajub.com/links/8kD2xQpLm \
  -H "Authorization: sk.xxxxxxxxxxxxxxxx…"

La réponse est l'état du lien, et c'est ce que vous interrogez pour savoir s'il encaisse toujours.

Réponse · 200 OK
{
"code": 200,
"status": "OK",
"message": "Payment link retrieved",
"link": {
"id": "8kD2xQpLm",
"type": "product",
"title": "Café Amina, 250 g",
"url": "https://wajub.link/cafe-amina",
"currency": "XAF",
"price": 3500,
"is_public": true,
"is_archived": false,
"expiry_date": null,
"sales_limit": null,
"created_at": "2026-09-11T14:00:00+00:00"
}
}

La liste fonctionne de la même façon. Elle accepte per_page, search, archived pour voir les liens archivés plutôt que les liens actifs, only_public, type (product, event, fundraising ou donation), et sort_by (created_at, updated_at, title ou type) avec sort_dir (desc ou asc). La liste suit le tri en premier, puis du plus récent au plus ancien entre liens à égalité. Pour les exports, un cursor parcourt la liste dans son ordre par défaut, du plus récent au plus ancien : avec un autre tri, la liste est paginée par numéro et ne porte pas de curseur. Un type, un sort_by ou un sort_dir hors de ces valeurs renvoie une 422 sur ce champ, comme un per_page hors limites.

GEThttps://api.wajub.com/links

Ce qui est réellement mesuré

Chaque visite qui passe les verrous d'accès est comptée, avec sa provenance (le referrer HTTP), le pays du visiteur et son type d'appareil.

Les visiteurs uniques sont comptés par jour : la même personne qui visite deux jours différents compte donc pour deux visiteurs uniques. Lisez ce nombre comme « uniques par jour », pas comme un nombre de personnes.

Une visite bloquée n'est pas une vue

Quand un lien est protégé par mot de passe, une tentative dont le mot de passe échoue n'est pas comptée. Une visite sur un lien non publié, expiré ou dont le plafond de vues est atteint ne l'est pas non plus. Le compteur n'avance que pour les visites qui ont réellement atteint la page, et c'est ce qui fait de max_views un plafond fiable.

Les chiffres, et où les lire

Liens → un lien → Vue d'ensemble est le seul endroit où ils apparaissent. Total des vues, Total des ventes et Revenu total couvrent toute la vie du lien, et Taux de conversion rapporte ces ventes à ses visiteurs. La période choisie, les 7, 14, 30 ou 90 derniers jours, ne change que les graphiques : les barres sous chaque total et Tendances des visites et des ventes. La transaction moyenne, les vues par transaction, les principaux pays et les appareils couvrent toujours les 30 derniers jours.

Attribuer un canal

Aucun paramètre d'URL ne marque une visite. Un ?src=whatsapp sur l'URL partagée n'est transmis nulle part : il n'atteint pas le paiement, il n'est pas enregistré sur la vue, et il n'apparaîtra dans aucun rapport.

Vous disposez à la place de deux choses qui fonctionnent.

Le referrer est capturé automatiquement : le trafic qui en porte un, depuis un lien dans une publication ou une page, affiche déjà son origine dans les statistiques. Le trafic venant de WhatsApp, d'un SMS ou d'un QR code n'a pas de referrer, et c'est en pratique l'essentiel du trafic.

Donc, pour tout ce que vous devez vraiment attribuer, créez un lien par canal. Même produit, même prix, custom_url différente : wajub.link/cafe-whatsapp, wajub.link/cafe-instagram. Chacun garde ses propres vues, son propre taux de conversion et son propre chiffre d'affaires, et la comparaison est exacte plutôt que déduite.

Un paiement fait sur un lien le désigne : son champ link est l'id du lien, dans l'API et dans les webhooks payment.*, si bien que votre handler sait de quel lien, donc de quel canal, vient l'argent. GET /payments n'a pas de filtre par lien : pour lister les paiements d'un lien, lisez Liens → un lien → Transactions dans le Dashboard, ou gardez le link de chaque paiement à son arrivée.

Quatre réglages mettent fin à la vie d'un lien, et ils ne le ferment pas de la même façon.

RéglageFermeLe payeur obtient
expiry_dateLa page, au moment que vous choisissez410, avant tout affichage
is_archivedLa page, immédiatement410, avant tout affichage
sales_limitLe paiement, pas la page422 au moment de payer, This payment link has reached its sales limit.
single_use_linkLa page, après son premier paiement réussiUne page qui indique que le lien a déjà servi à payer

La différence compte. Un lien expiré ou archivé ne s'ouvre jamais : le payeur voit une page fermée et tout s'arrête là. Un lien qui a atteint son plafond de ventes s'ouvre toujours normalement, affiche le produit et le bouton, et ne refuse qu'au moment du paiement. Si vous vendez une quantité fixe, attendez-vous à ce que des payeurs tombent sur ce message plutôt que sur une page fermée.

Un lien à usage unique accepte un seul payeur à la fois. Pendant que quelqu'un paie par ce lien, un autre payeur apprend qu'un paiement par ce lien est en cours, et que le lien rouvre si ce paiement n'aboutit pas. Une fois un paiement réussi, le lien reste fermé, même si vous le remboursez. Aucun champ du lien n'indique qu'il est fermé : payment.succeeded, dont le link est l'id du lien, est votre signal. Si un second paiement aboutit malgré tout par ce lien, comme une tentative antérieure que l'opérateur confirme en retard, le propriétaire, les administrateurs et les membres finance de votre équipe reçoivent un e-mail qui nomme le paiement à rembourser : Wajub ne le rembourse pas de lui-même.

link.expired est envoyé une fois, dans la journée qui suit le passage de l'expiry_date d'un lien, sauf si le lien est archivé entre-temps, avec le lien dans data. L'archivage envoie link.updated, tout comme une vente ou un remboursement qui change le stock d'un produit. Atteindre le plafond de ventes et fermer un lien à usage unique n'envoient aucun événement propre.

L'archivage est l'option réversible : remettez is_archived à false via PUT /links/{id} et le lien encaisse de nouveau, en conservant tout ce qu'il a déjà collecté. expiry_date est une condition plutôt qu'un état : la repousser rouvre un lien qui s'était fermé à cause d'elle.

La dépublication avec is_public: false ferme aussi un lien, mais répond 404 au lieu de 410, car un lien non publié doit donner l'impression de n'avoir jamais existé. Les verrous d'accès sont décrits dans Personnalisation.

Ce que rend un remboursement

Un lien compte ce qu'il a gardé, pas ce qu'il a encaissé un jour. Un remboursement réussi, ou un litige que vous perdez, rend ce que le paiement avait pris :

  • Un billet ou une unité dont le prix entier est remboursé ne compte plus dans event_capacity, sales_limit ni dans la quantité de son type de billet, et revient dans le stock d'un produit physique. Le remboursement d'une partie du prix d'un billet garde le billet : trois billets à 3 000 XAF remboursés de 3 000 XAF laissent deux billets vendus.
  • Sur une collecte ou une page de dons, le total collecté, la progression vers l'objectif et le nombre de contributeurs baissent de ce qui a été remboursé. Un don remboursé en entier quitte le mur des donateurs.
  • Un paiement perdu dans un litige ne compte plus, comme un remboursement complet. Un litige encore ouvert, ou que vous avez gagné, ne change rien.

Un remboursement qui échoue, ou qui est encore en cours, ne change rien non plus.

L'onglet Webhooks d'un lien, dans le Dashboard, envoie les événements propres à ce lien à une adresse à vous, avec un secret de signature qui lui est propre. Ils s'ajoutent aux webhooks de votre compte, qui couvrent tous les paiements.

ÉvénementEnvoyé quand
payment.successUn paiement par le lien a réussi, quel que soit son type
ticket_purchasedDes billets ont été achetés sur un lien d'événement
donation.receivedUn don a été reçu sur un lien de don ou de collecte
order.placedUne commande a été payée sur un lien de produit
payment.refundedUn remboursement d'un de ses paiements a réussi, une fois par remboursement, ou un litige sur l'un d'eux a été perdu
link_expiredL'expiry_date du lien est passée

Un paiement qui échoue n'envoie rien. Les paiements sandbox ne partent que vers les endpoints d'un lien sandbox, avec sandbox: true. En sandbox, un remboursement ou un litige accepté depuis le tableau de bord n'envoie pas de payment.refunded.

Chaque envoi est un POST avec un corps JSON, signé dans X-Webhook-Signature et nommé dans X-Webhook-Event. Chaque événement porte en plus un id dans le corps, et la même valeur dans X-Webhook-Id :

En-têteCe que c'est
X-Webhook-SignatureHMAC-SHA256 en hexadécimal du corps brut, avec le secret de signature de l'endpoint
X-Webhook-EventL'événement, comme dans le corps
X-Webhook-IdL'id de l'événement, comme dans le corps
import crypto from 'node:crypto';

app.post('/webhooks/link', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', process.env.LINK_WEBHOOK_SECRET).update(req.body).digest('hex');
  const received = String(req.headers['x-webhook-signature'] ?? '');

  if (received.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
    return res.sendStatus(400);
  }

  const event = JSON.parse(req.body);
  // Ignorez un event.id déjà traité, puis livrez.
  res.sendStatus(200);
});

Répondez par un 2xx une fois l'événement enregistré. Un envoi qui échoue est retenté, pendant 72 heures au plus, si bien qu'un même événement peut arriver plusieurs fois. Un événement garde son id à chaque fois : gardez ceux que vous avez traités et ignorez-les. Un envoi renvoyé depuis l'onglet a le même corps et le même X-Webhook-Id.

timestamp est le moment où l'événement s'est produit, pas celui de l'envoi : une nouvelle tentative, ou un envoi renvoyé depuis l'onglet, le garde.

Chaque événement de paiement porte le lien et le paiement. Le payeur n'est désigné que par name et email : un envoi ne contient jamais son numéro de téléphone ni sa carte. Ce qui a été saisi au paiement arrive avec toute suite de sept chiffres ou plus masquée, sous la forme 23••••89, y compris au milieu d'une phrase ; answers ne contient que les questions du lien, chacune coupée à 500 caractères.

ticket_purchased
{
"id": "lwe_5b3c2f1e-8d0a-5c4b-9e7f-2a1d6c0b9e84",
"event": "ticket_purchased",
"link_id": "9f3a1c52-6b1e-4f7d-a1c2-7e0d4b8f2a61",
"link_uid": "8kD2xQpLm",
"link": {
"id": "9f3a1c52-6b1e-4f7d-a1c2-7e0d4b8f2a61",
"uid": "8kD2xQpLm",
"title": "Soirée Afrobeats",
"type": "event"
},
"sandbox": false,
"timestamp": "2026-10-23T18:04:11+00:00",
"payment": {
"id": "trx_CSUGajfv9xh0XQ5wu2lx",
"reference": "order-4172",
"amount": 10000,
"currency": "XAF",
"status": "succeeded",
"amount_refunded": 0,
"dispute_lost": false,
"quantity": 2,
"quantity_kept": 2,
"channel": "cm.mtn",
"customer": {
"id": "cus_Qe7Tb2Wn5Lr9Xc4Mv8Hk1Zs3",
"name": "Ada Obi",
"email": "ada@example.com"
},
"answers": {
"company": "Acme"
},
"paid_at": "2026-10-23T18:04:09+00:00",
"created_at": "2026-10-23T18:02:47+00:00"
},
"ticket": {
"ticket_type": {
"id": "3e1b7a90-2c4d-4f6e-8a1b-5d9c0e2f7b13",
"name": "VIP"
},
"quantity": 2,
"attendees": [
{
"Nom complet": "Ada Obi",
"Téléphone": "23••••33"
},
{
"Nom complet": "Ben Eko"
}
],
"event_start_date": "2026-11-07T20:00:00+00:00"
}
}

link_uid est l'id du lien dans l'API, et payment.id celui du paiement. quantity_kept est ce que le paiement détient encore une fois ses remboursements et ses litiges comptés, comme plus haut. Chaque événement ajoute un objet à côté de payment :

ÉvénementAjoute
payment.successRien de plus
ticket_purchasedticket : ticket_type (id, name), quantity, attendees, event_start_date
donation.receiveddonation : amount, currency, anonymous, recurring_interval
order.placedorder : product_type, quantity, variant, shipping_address avec son phone masqué
payment.refundedrefund : id, amount, currency, reason, created_at. payment.status vaut alors partially_refunded ou refunded. Pour un litige perdu, dispute à la place de refund : id, amount, currency, reason, status, created_at, avec payment.dispute_lost à true

anonymous signifie que le donateur a demandé à ne pas figurer sur le mur public des donateurs : vous recevez tout de même son nom. Un litige perdu envoie payment.refunded une fois ; si un remboursement le règle ensuite, ce remboursement envoie le sien, avec un autre id. link_expired porte id, event, link_id, link_uid, le lien avec son expiry_date, et timestamp.

Le rapprochement des fonds

Les paiements d'un lien sont des paiements ordinaires. Ils arrivent dans votre liste Payments, déclenchent payment.succeeded, sont réglés sur votre solde et se remboursent comme les autres : il n'y a donc rien de propre aux liens à rapprocher.

Le webhook transporte ce que le payeur a saisi dans vos extra_fields, imbriqué sous metadata.extra_fields, ainsi que le link dont vient le paiement.

Que pensez-vous de ce contenu ?