API InvexQR

Version 2026-10 · REST + JSON · réservée à la formule Business · fichier OpenAPI 3.1

Créer une clé

Authentification

Créez une clé dans Espace client → Intégrations → API (propriétaire ou administrateur), puis envoyez-la dans l’en-tête :

Authorization: Bearer iqr_live_XXXXXXXX_YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY

Chaque clé porte des permissions (qr:read, qr:write, campaigns:read, campaigns:write, stats:read, webhooks:manage), peut expirer et se révoque instantanément. Elle ne doit être utilisée que côté serveur : jamais dans une page web ou une application mobile.

curl https://invexqr.com/api/v1/me -H "Authorization: Bearer $INVEXQR_API_KEY"

Erreurs

Les erreurs ont toujours la même forme. Le code est stable (à utiliser dans votre logique), le message est en français, fields détaille les champs invalides.

{
    "error": {
        "code": "validation_failed",
        "message": "Les adresses IP et réseaux locaux ne sont pas autorisés.",
        "fields": {
            "destination": "Les adresses IP et réseaux locaux ne sont pas autorisés."
        },
        "request_id": "req_K6a5hEsCx34Z3KwkLEY6"
    }
}
400invalid_json, invalid_idempotency_key
401authentication_required, invalid_api_key, api_key_revoked, api_key_expired
403plan_required, insufficient_scope, quota_exceeded, organization_suspended, forbidden
404not_found — y compris pour une ressource d’une autre organisation
409 / 415 / 413conflict, idempotency_in_progress, unsupported_media_type, payload_too_large
422validation_failed, idempotency_key_reused, format_not_supported, invalid_scan_token…
429rate_limited, monthly_quota_exceeded, edit_limit

Limites et quotas

Débit par clé (120 requêtes par minute en Business) et quota mensuel par organisation (100 000 requêtes, remis à zéro le 1er du mois, UTC). Chaque réponse indique l’état :

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1792030709      (horodatage Unix)
X-Quota-Limit: 100000
X-Quota-Remaining: 98412

Au-delà : réponse 429 avec Retry-After (secondes). Les quotas de la formule (nombre de QR dynamiques, lignes par création en masse, historique des statistiques…) s’appliquent exactement comme dans l’interface.

Idempotence

Les créations (POST) acceptent l’en-tête Idempotency-Key. Si une requête est rejouée avec la même clé et le même corps dans les 24 heures (coupure réseau, nouvel essai de Make ou n8n…), InvexQR renvoie la réponse initiale avec l’en-tête Idempotent-Replayed: true, sans créer de doublon. La même clé avec un corps différent est refusée (idempotency_key_reused).

Conventions

  • URL de base : https://invexqr.com/api/v1. Corps des requêtes en JSON (Content-Type: application/json).
  • Réponses : {"data": …}, avec meta pour la pagination (?page=, ?per_page= jusqu’à 200).
  • Dates et heures en UTC, ISO 8601 (2026-10-15T09:41:07Z). Les périodes des statistiques (from, to) sont des jours locaux du fuseau de l’organisation.
  • Montants entiers en FCFA (XAF). Statistiques : scans filtrés des robots, aperçus de liens et doublons (méthode de comptage).
  • Les champs existants ne sont jamais renommés ; de nouveaux champs peuvent apparaître : ignorez ceux que vous ne connaissez pas.

Plateforme

GET /api/v1/public/stats

Compteurs publics de la plateforme

Totaux agrégés et anonymes (QR codes, scans, comptes, organisations actives, campagnes, intégrations, requêtes API), calculés sur les données réelles et mis en cache. Chaque compteur est accompagné de sa définition. Un compteur sous son seuil de publication n’est pas renvoyé. Sans authentification ; 30 requêtes par minute et par visiteur.

Réponses : 200 · 429

Compte

GET /api/v1/me

Organisation, clé et consommation

Retourne l’organisation liée à la clé, ses permissions, la consommation du mois et les quotas utiles.

Réponses : 200 · 401 · 403 · 429

GET /api/v1/domains

Domaines personnalisés

Domaines déclarés par l’organisation. Seuls les domaines active peuvent être utilisés à la création d’un QR.

Permission requise : qr:read.

Réponses : 200 · 401 · 403 · 429

QR codes

GET /api/v1/qr

Lister les QR codes

Liste paginée, du plus récent au plus ancien.

Permission requise : qr:read.

ParamètreOùTypeDescription
pageURLintegerPage (à partir de 1).
per_pageURLintegerÉléments par page (200 au maximum).
kindURLstring (static, redirect, multilink, file, coupon)Type de QR.
statusURLstring (active, paused, suspended)Statut.
campaign_idURLintegerQR associés en ce moment à une campagne (supports et QR existants).
batch_idURLintegerCodes d’un lot créé en masse.
searchURLstringRecherche dans le nom (ou identifiant court exact).

Réponses : 200 · 401 · 403 · 429 · 422

POST /api/v1/qr

Créer un QR code

QR dynamique de redirection (kind: redirect, par défaut) ou statique (kind: static, contenu encodé tel quel).

Avec campaign_id, le QR devient un support de la campagne : UTM ajoutés automatiquement (channel = type de support).

Les quotas de la formule s’appliquent comme dans l’interface.

Permission requise : qr:write.

ParamètreOùTypeDescription
Idempotency-Keyen-têtestringIdentifiant unique de l’opération (1 à 100 caractères). Une requête rejouée avec la même clé et le même corps renvoie la réponse initiale (en-tête Idempotent-Replayed) pendant 24 h.
Champs du corps
ChampTypeDescription
kindstring (redirect, static)
namestring
destinationstringObligatoire pour kind=redirect (https:// ajouté si absent).
contentstringObligatoire pour kind=static (ex. WIFI:T:WPA;S:Boutique;P:motdepasse;;).
data_typestringType de contenu statique (texte libre, informatif).
styleobjectStyle graphique (voir la page Développeurs).
domainstringNom d’hôte d’un domaine personnalisé actif, ou « platform ». Défaut : domaine par défaut de l’organisation.
expires_atstring | null
expired_urlstring
app_linksobjectkind=redirect : iPhone vers « ios » (App Store), Android vers « android » (Google Play), autres appareils vers « destination ».
campaign_idinteger
channelstring (affiche, flyer, depliant, carte_visite, emballage, etiquette, facture, ticket_caisse, vitrine, panneau, presse, television, radio, evenement, salon, sms, email, reseaux_sociaux, publicite_numerique, catalogue, menu, support_commercial)
support_costinteger
curl -X POST https://invexqr.com/api/v1/qr \
  -H "Authorization: Bearer $INVEXQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Affiche vitrine Akwa","destination":"https://boutique.exemple.cm/promo","style":{"fg":"#231F20","frame":"bottom","frameText":"Scannez-moi"}}'

Réponses : 201 · 401 · 403 · 429 · 400 · 422

POST /api/v1/qr/bulk

Créer des QR codes en masse

Tout ou rien : si une ligne est invalide, aucun code n’est créé et les erreurs sont détaillées par ligne (items.N.destination). Le nombre de lignes est limité par la formule (max_bulk_rows).

Permission requise : qr:write.

ParamètreOùTypeDescription
Idempotency-Keyen-têtestringIdentifiant unique de l’opération (1 à 100 caractères). Une requête rejouée avec la même clé et le même corps renvoie la réponse initiale (en-tête Idempotent-Replayed) pendant 24 h.
Champs du corps
ChampTypeDescription
modestring (dynamic, static)
namestring
campaign_idinteger
domainstring
items requisarray
curl -X POST https://invexqr.com/api/v1/qr/bulk \
  -H "Authorization: Bearer $INVEXQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode":"dynamic","name":"Chevalets des tables","items":[{"name":"Table 1","destination":"https://resto.exemple.cm/menu?table=1"},{"name":"Table 2","destination":"https://resto.exemple.cm/menu?table=2"}]}'

Réponses : 201 · 401 · 403 · 429 · 400 · 422

GET /api/v1/qr/{id}

Détail d’un QR code

Pour une page MultiLink, la réponse inclut aussi page et links.

Permission requise : qr:read.

ParamètreOùTypeDescription
idcheminintegerIdentifiant

Réponses : 200 · 401 · 403 · 429 · 404

PATCH /api/v1/qr/{id}

Modifier un QR code

Mise à jour partielle : name, destination (QR de redirection : le code imprimé redirige aussitôt vers la nouvelle adresse, changement historisé), status (active ou paused), style, expires_at (ISO 8601, null pour retirer) et expired_url.

Permission requise : qr:write.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
Champs du corps
ChampTypeDescription
namestring
destinationstring
statusstring (active, paused)
styleobjectStyle graphique (voir la page Développeurs).
expires_atstring | null
expired_urlstring
app_linksobject
curl -X PATCH https://invexqr.com/api/v1/qr/12 \
  -H "Authorization: Bearer $INVEXQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"destination":"https://boutique.exemple.cm/nouvelle-promo"}'

Réponses : 200 · 401 · 403 · 429 · 400 · 404 · 422

DELETE /api/v1/qr/{id}

Supprimer un QR code

Suppression définitive : un QR dynamique imprimé affichera « page introuvable ».

Permission requise : qr:write.

ParamètreOùTypeDescription
idcheminintegerIdentifiant

Réponses : 204 · 401 · 403 · 429 · 404

GET /api/v1/qr/{id}/image

Image du QR code

Rendu serveur avec le style enregistré (couleurs, dégradé, formes, logo, cadre). Format QR Code uniquement. Réponse mise en cache (ETag).

Permission requise : qr:read.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
formatURLstring (png, svg)Format de l’image.
sizeURLintegerLargeur en pixels (64 à 2000).
marginURLintegerMarge en modules (0 à 10).
eccURLstring (L, M, Q, H)Niveau de correction d’erreur minimal.
frameURLstring (none)`none` pour retirer le cadre.

Réponses : 200 · 304 · 401 · 403 · 429 · 404 · 422

GET /api/v1/qr/{id}/history

Historique des destinations

Les 200 derniers changements (destination et liens MultiLink).

Permission requise : qr:read.

ParamètreOùTypeDescription
idcheminintegerIdentifiant

Réponses : 200 · 401 · 403 · 429 · 404

GET /api/v1/qr/{id}/campaigns

Campagnes d’un QR code

Associations du QR (en cours et passées) avec les scans attribués à chaque campagne, et le total global du QR (toutes périodes, toutes campagnes), qu’une association ne modifie jamais.

Permission requise : qr:read.

ParamètreOùTypeDescription
idcheminintegerIdentifiant

Réponses : 200 · 401 · 403 · 429 · 404

Statistiques

GET /api/v1/qr/{id}/stats

Statistiques d’un QR code

Scans filtrés (robots, aperçus, doublons exclus), visiteurs uniques estimés, clics, répartitions.

Permission requise : stats:read.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
fromURLstringPremier jour (AAAA-MM-JJ, fuseau de l’organisation). Défaut : il y a 29 jours.
toURLstringDernier jour inclus (AAAA-MM-JJ). Défaut : aujourd’hui.

Réponses : 200 · 401 · 403 · 429 · 404 · 422

GET /api/v1/stats

Statistiques de l’organisation

Totaux, série quotidienne, répartitions et classement des QR. Filtrable par QR ou campagne.

Permission requise : stats:read.

ParamètreOùTypeDescription
fromURLstringPremier jour (AAAA-MM-JJ, fuseau de l’organisation). Défaut : il y a 29 jours.
toURLstringDernier jour inclus (AAAA-MM-JJ). Défaut : aujourd’hui.
qr_idURLintegerLimiter à un QR.
campaign_idURLintegerLimiter aux scans attribués à une campagne (chaque QR sur ses jours d’association).

Réponses : 200 · 401 · 403 · 429 · 404 · 422

GET /api/v1/stats/geo

Répartition géographique

Pays et villes **estimés** à partir de l’adresse IP au moment du scan (base locale ; adresse non conservée, jamais transmise à un tiers). La ville n’est donnée que lorsque la précision le permet (pas pour les réseaux mobiles) ; VPN, relais et hébergeurs sont « inconnus ». totals.scans = with_country + unknown + unmeasured (scans antérieurs à la géolocalisation : jamais localisés après coup).

Permission requise : stats:read.

ParamètreOùTypeDescription
fromURLstringPremier jour (AAAA-MM-JJ, fuseau de l’organisation). Défaut : il y a 29 jours.
toURLstringDernier jour inclus (AAAA-MM-JJ). Défaut : aujourd’hui.
qr_idURLintegerLimiter à un QR.
campaign_idURLintegerLimiter aux scans attribués à une campagne.

Réponses : 200 · 401 · 403 · 429 · 404 · 422

GET /api/v1/events

Exporter les événements

Événements bruts enregistrés par le serveur, sans aucune donnée personnelle (ni IP, ni empreinte visiteur). Par défaut, seuls les événements retenus (status=valid). Pagination par curseur : rappelez avec after_id = meta.next_after_id tant que meta.has_more vaut true.

Permission requise : stats:read.

ParamètreOùTypeDescription
fromURLstringPremier jour (AAAA-MM-JJ, fuseau de l’organisation). Défaut : il y a 29 jours.
toURLstringDernier jour inclus (AAAA-MM-JJ). Défaut : aujourd’hui.
typeURLstring (scan, visit, click)Type.
statusURLstring (valid, bot, preview, prefetch, duplicate, rate, all)Classification (`all` pour tout).
qr_idURLintegerLimiter à un QR.
after_idURLintegerCurseur.
limitURLintegerTaille de page (1000 au maximum).

Réponses : 200 · 401 · 403 · 429 · 422

MultiLink

Campagnes

GET /api/v1/campaigns

Lister les campagnes

Permission requise : campaigns:read.

ParamètreOùTypeDescription
pageURLintegerPage (à partir de 1).
per_pageURLintegerÉléments par page (200 au maximum).
statusURLstring (active, paused, archived, draft)Statut.

Réponses : 200 · 401 · 403 · 429

POST /api/v1/campaigns

Créer une campagne

Associez ensuite des QR existants avec POST /campaigns/{id}/qr, ou créez un support (QR) par emplacement avec POST /campaigns/{id}/supports.

Permission requise : campaigns:write.

ParamètreOùTypeDescription
Idempotency-Keyen-têtestringIdentifiant unique de l’opération (1 à 100 caractères). Une requête rejouée avec la même clé et le même corps renvoie la réponse initiale (en-tête Idempotent-Replayed) pendant 24 h.
Champs du corps
ChampTypeDescription
name requisstring
descriptionstring
objectivestring (notoriete, trafic, prospects, ventes, fidelisation, evenement)
owner_namestring
start_datestring
end_datestring
budgetinteger
audiencestring
destination requisstring
utm_enabledboolean
conversion_goalstring
conversion_trackingboolean
curl -X POST https://invexqr.com/api/v1/campaigns \
  -H "Authorization: Bearer $INVEXQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Rentrée 2026","objective":"ventes","destination":"https://boutique.exemple.cm/rentree","budget":250000,"conversion_tracking":true}'

Réponses : 201 · 401 · 403 · 429 · 400 · 422

GET /api/v1/campaigns/{id}

Campagne et performance

Performance par QR sur la période (supports et QR existants associés) : scans attribués, visiteurs, conversions, valeur, coût par scan et par conversion, ROI (calculés seulement si les données existent), et total global de chaque QR.

Permission requise : campaigns:read.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
fromURLstringPremier jour (AAAA-MM-JJ, fuseau de l’organisation). Défaut : il y a 29 jours.
toURLstringDernier jour inclus (AAAA-MM-JJ). Défaut : aujourd’hui.

Réponses : 200 · 401 · 403 · 429 · 404 · 422

PATCH /api/v1/campaigns/{id}

Modifier une campagne

Mise à jour partielle. Changer la destination ou les UTM recalcule la destination des supports (historisé). status : active, paused, archived.

Permission requise : campaigns:write.

ParamètreOùTypeDescription
idcheminintegerIdentifiant

Réponses : 200 · 401 · 403 · 429 · 400 · 404 · 422

POST /api/v1/campaigns/{id}/supports

Ajouter un support (QR)

Crée un QR dynamique distinct pour un emplacement (affiche, flyer…), avec ses UTM.

Permission requise : campaigns:write.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
Idempotency-Keyen-têtestringIdentifiant unique de l’opération (1 à 100 caractères). Une requête rejouée avec la même clé et le même corps renvoie la réponse initiale (en-tête Idempotent-Replayed) pendant 24 h.
Champs du corps
ChampTypeDescription
channel requisstring (affiche, flyer, depliant, carte_visite, emballage, etiquette, facture, ticket_caisse, vitrine, panneau, presse, television, radio, evenement, salon, sms, email, reseaux_sociaux, publicite_numerique, catalogue, menu, support_commercial)
labelstring
destinationstringAdresse propre au support (sinon celle de la campagne).
costintegerCoût du support en FCFA (impression, emplacement).
domainstring
curl -X POST https://invexqr.com/api/v1/campaigns/12/supports \
  -H "Authorization: Bearer $INVEXQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel":"flyer","label":"Flyers marché Mboppi","cost":45000}'

Réponses : 201 · 401 · 403 · 429 · 400 · 404 · 422

GET /api/v1/campaigns/{id}/qr

QR codes associés

Supports et QR existants de la campagne. state=all inclut les associations passées (historique).

Permission requise : campaigns:read.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
stateURLstring (current, all)Associations en cours ou toutes.

Réponses : 200 · 401 · 403 · 429 · 404 · 422

POST /api/v1/campaigns/{id}/qr

Associer des QR codes existants

Associe des QR de votre bibliothèque à la campagne, sans les recréer ni les modifier (même image, même lien, même destination). Leurs scans sont attribués à la campagne à partir de counted_from (today par défaut, start = début de la campagne, ou une date) ; leurs statistiques globales ne changent pas.

Un QR déjà associé n’est pas dupliqué (already_attached). Un identifiant inexistant ou d’une autre organisation est listé dans not_found et n’est jamais associé. Seuls les QR dynamiques non suspendus sont associables (refused). Un même QR peut appartenir à plusieurs campagnes.

Permission requise : campaigns:write.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
Idempotency-Keyen-têtestringIdentifiant unique de l’opération (1 à 100 caractères). Une requête rejouée avec la même clé et le même corps renvoie la réponse initiale (en-tête Idempotent-Replayed) pendant 24 h.
Champs du corps
ChampTypeDescription
qr_ids requisarray
counted_fromstring`today`, `start` ou AAAA-MM-JJ (entre le début de la campagne et aujourd’hui).
pass_tokenbooleanTransmettre le jeton iqr à la destination (QR de redirection, campagne avec suivi des conversions).
costintegerCoût par QR pour cette campagne, en FCFA.
curl -X POST https://invexqr.com/api/v1/campaigns/12/qr \
  -H "Authorization: Bearer $INVEXQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"qr_ids":[311,312],"counted_from":"today"}'

Réponses : 201 · 200 · 401 · 403 · 429 · 400 · 404 · 409 · 422

PATCH /api/v1/campaigns/{id}/qr/{qr}

Modifier une association

cost (FCFA, null pour effacer) et pass_token d’une association en cours.

Permission requise : campaigns:write.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
qrcheminintegerIdentifiant du QR
Champs du corps
ChampTypeDescription
costinteger | null
pass_tokenboolean
curl -X PATCH https://invexqr.com/api/v1/campaigns/12/qr/12 \
  -H "Authorization: Bearer $INVEXQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cost":0}'

Réponses : 200 · 401 · 403 · 429 · 400 · 404 · 422

DELETE /api/v1/campaigns/{id}/qr/{qr}

Retirer un QR de la campagne

Ferme l’association : le QR n’est ni supprimé ni modifié et ses statistiques globales restent intactes. Les scans déjà attribués restent dans l’historique de la campagne, sauf avec void=true (association faite par erreur : aucun jour attribué).

Permission requise : campaigns:write.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
qrcheminintegerIdentifiant du QR
voidURLbooleanAnnuler l’attribution.

Réponses : 200 · 401 · 403 · 429 · 404

POST /api/v1/campaigns/{id}/conversions

Déclarer une conversion

Conversion déclarée (vente en boutique, commande par téléphone…), toujours distinguée des conversions mesurées.

Permission requise : campaigns:write.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
Idempotency-Keyen-têtestringIdentifiant unique de l’opération (1 à 100 caractères). Une requête rejouée avec la même clé et le même corps renvoie la réponse initiale (en-tête Idempotent-Replayed) pendant 24 h.
Champs du corps
ChampTypeDescription
datestring
quantityinteger
valueintegerMontant en FCFA.
qr_idinteger
notestring
curl -X POST https://invexqr.com/api/v1/campaigns/12/conversions \
  -H "Authorization: Bearer $INVEXQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"quantity":3,"value":45000,"note":"Ventes en boutique du samedi"}'

Réponses : 201 · 401 · 403 · 429 · 400 · 404 · 422

POST /api/v1/conversions

Enregistrer une conversion mesurée

À appeler par votre serveur quand un visiteur venu d’un scan convertit (commande payée…). scan_token est la valeur du paramètre iqr ajouté à l’adresse de destination. Une seule conversion par scan, dans les 30 jours. Rejouer l’appel renvoie status: duplicate.

Permission requise : campaigns:write.

Champs du corps
ChampTypeDescription
scan_token requisstring
valueinteger
quantityinteger
campaign_idintegerNécessaire seulement si le QR scanné faisait partie de plusieurs campagnes ce jour-là (erreur `campaign_required`).
curl -X POST https://invexqr.com/api/v1/conversions \
  -H "Authorization: Bearer $INVEXQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scan_token":"2n9c.5f0e4c1a9b7d","value":15000}'

Réponses : 201 · 200 · 401 · 403 · 429 · 400 · 422

Webhooks

GET /api/v1/webhooks

Lister les webhooks

Permission requise : webhooks:manage.

Réponses : 200 · 401 · 403 · 429

POST /api/v1/webhooks

Créer un webhook

Le secret de signature (secret) n’est renvoyé qu’à la création ou à sa régénération : conservez-le. Adresse HTTPS publique obligatoire.

Permission requise : webhooks:manage.

ParamètreOùTypeDescription
Idempotency-Keyen-têtestringIdentifiant unique de l’opération (1 à 100 caractères). Une requête rejouée avec la même clé et le même corps renvoie la réponse initiale (en-tête Idempotent-Replayed) pendant 24 h.
Champs du corps
ChampTypeDescription
url requisstring
events requisobjet
descriptionstring
curl -X POST https://invexqr.com/api/v1/webhooks \
  -H "Authorization: Bearer $INVEXQR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://crm.exemple.cm/hooks/invexqr","events":["scan.recorded","conversion.recorded"]}'

Réponses : 201 · 401 · 403 · 429 · 400 · 422

GET /api/v1/webhooks/{id}

Détail d’un webhook

Permission requise : webhooks:manage.

ParamètreOùTypeDescription
idcheminintegerIdentifiant

Réponses : 200 · 401 · 403 · 429 · 404

PATCH /api/v1/webhooks/{id}

Modifier un webhook

status: active réactive un webhook désactivé et remet son compteur d’échecs à zéro.

Permission requise : webhooks:manage.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
Champs du corps
ChampTypeDescription
urlstring
eventsarray
descriptionstring
statusstring (active, disabled)

Réponses : 200 · 401 · 403 · 429 · 400 · 404 · 422

DELETE /api/v1/webhooks/{id}

Supprimer un webhook

Permission requise : webhooks:manage.

ParamètreOùTypeDescription
idcheminintegerIdentifiant

Réponses : 204 · 401 · 403 · 429 · 404

POST /api/v1/webhooks/{id}/test

Envoyer un événement de test

Envoie immédiatement test.ping et retourne le résultat de la livraison.

Permission requise : webhooks:manage.

ParamètreOùTypeDescription
idcheminintegerIdentifiant

Réponses : 200 · 401 · 403 · 429 · 404

POST /api/v1/webhooks/{id}/secret

Régénérer le secret

L’ancien secret cesse immédiatement d’être utilisé.

Permission requise : webhooks:manage.

ParamètreOùTypeDescription
idcheminintegerIdentifiant

Réponses : 200 · 401 · 403 · 429 · 404

GET /api/v1/webhooks/{id}/deliveries

Journal des livraisons

Conservé 30 jours.

Permission requise : webhooks:manage.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
pageURLintegerPage (à partir de 1).
per_pageURLintegerÉléments par page (200 au maximum).
statusURLstring (pending, success, failed)Statut.

Réponses : 200 · 401 · 403 · 429 · 404 · 422

POST /api/v1/webhooks/{id}/deliveries/{did}/retry

Relancer une livraison

Renvoie immédiatement l’événement (même identifiant, nouvelle signature).

Permission requise : webhooks:manage.

ParamètreOùTypeDescription
idcheminintegerIdentifiant
didcheminintegerIdentifiant de la livraison

Réponses : 200 · 401 · 403 · 429 · 404 · 409

Webhooks

Abonnez une adresse HTTPS à des événements (Intégrations → Webhooks ou POST /api/v1/webhooks). Chaque événement est envoyé en POST JSON. Répondez 2xx en moins de 5 secondes ; sinon nouvelles tentatives après 1 min, 5 min, 30 min, 2 h, 6 h, 12 h et 24 h. Une même livraison peut arriver deux fois : dédoublonnez sur id.

ÉvénementDéclenché quand
scan.recordedScan ou visite comptabilisé (robots et doublons exclus)
click.recordedClic sur un lien MultiLink
conversion.recordedConversion enregistrée (mesurée ou déclarée)
coupon.issuedCode de coupon délivré à un client
coupon.redeemedCoupon validé en caisse
qr.createdQR code créé
qr.updatedQR code modifié (destination, statut, nom)
qr.deletedQR code supprimé
batch.createdLot de QR codes créé en masse
campaign.createdCampagne créée
campaign.updatedCampagne modifiée
campaign.qr_attachedQR codes existants associés à une campagne
campaign.qr_detachedQR code retiré d’une campagne (le QR n’est pas supprimé)
alert.triggeredAlerte de performance déclenchée
test.pingBouton « Envoyer un test » (toujours envoyé, sans abonnement)

Exemple (scan.recorded) :

{
    "id": "evt_8Hq2mPzR4kTn6wXc9bVd3sLfGa",
    "type": "scan.recorded",
    "created_at": "2026-10-15T09:41:07Z",
    "api_version": "2026-10",
    "organization_id": 42,
    "data": {
        "event_id": 98123,
        "occurred_at": "2026-10-15T09:41:07Z",
        "via": "qr",
        "qr": {
            "id": 311,
            "name": "Affiche vitrine Akwa",
            "kind": "redirect",
            "slug": "Hwf5XcD8",
            "campaign_id": 12,
            "short_url": "https://go.invexqr.com/q/Hwf5XcD8"
        },
        "device": "mobile",
        "os": "Android",
        "browser": "Chrome",
        "country": "CM",
        "city": null,
        "referrer_host": null,
        "variant_id": null
    }
}

En-têtes : InvexQR-Signature, InvexQR-Event-Id, InvexQR-Event-Type, InvexQR-Delivery-Id, InvexQR-Attempt. Aucune donnée personnelle des visiteurs n’est transmise (ni IP, ni identifiant).

Vérifier la signature

InvexQR-Signature: t=<horodatage>,v1=<signature> où la signature est le HMAC-SHA256 hexadécimal, avec votre secret whsec_…, de la chaîne t + "." + corps brut. Refusez les messages de plus de 5 minutes.

PHP
<?php
// Réception d'un webhook InvexQR (PHP 7.4+). Secret : variable d'environnement INVEXQR_WEBHOOK_SECRET.
$secret = getenv('INVEXQR_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_INVEXQR_SIGNATURE'] ?? '';

if (!preg_match('/(?:^|,)t=(\d+)/', $header, $t)
    || !preg_match('/(?:^|,)v1=([a-f0-9]{64})/', $header, $v)
    || abs(time() - (int) $t[1]) > 300
    || !hash_equals(hash_hmac('sha256', $t[1] . '.' . $body, $secret), $v[1])) {
    http_response_code(400);
    exit('Signature invalide');
}

$event = json_decode($body, true);
// Traitez chaque $event['id'] une seule fois (une livraison peut être répétée).
// $event['type'] : scan.recorded, conversion.recorded, coupon.redeemed…  — $event['data'] : contenu.
http_response_code(204);
Node.js
// Réception d'un webhook InvexQR (Node.js 18+, sans dépendance). Secret : INVEXQR_WEBHOOK_SECRET.
const http = require('node:http');
const crypto = require('node:crypto');

function verify(secret, body, header, toleranceSec = 300) {
  const t = /(?:^|,)t=(\d+)/.exec(header || '');
  const v = /(?:^|,)v1=([a-f0-9]{64})/.exec(header || '');
  if (!t || !v || Math.abs(Date.now() / 1000 - Number(t[1])) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(t[1] + '.' + body).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v[1]));
}

http.createServer((req, res) => {
  let body = '';
  req.setEncoding('utf8');
  req.on('data', (c) => { body += c; });
  req.on('end', () => {
    if (!verify(process.env.INVEXQR_WEBHOOK_SECRET, body, req.headers['invexqr-signature'])) {
      res.writeHead(400).end('Signature invalide');
      return;
    }
    const event = JSON.parse(body); // event.id, event.type, event.data
    console.log(JSON.stringify({ id: event.id, type: event.type }));
    res.writeHead(204).end();
  });
}).listen(Number(process.env.PORT || 3000), '127.0.0.1');

module.exports = { verify };
Python
# Réception d'un webhook InvexQR (Python 3.8+, bibliothèque standard). Secret : INVEXQR_WEBHOOK_SECRET.
import hashlib, hmac, json, os, re, sys, time
from http.server import BaseHTTPRequestHandler, HTTPServer


def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
    t = re.search(r'(?:^|,)t=(\d+)', header or '')
    v = re.search(r'(?:^|,)v1=([a-f0-9]{64})', header or '')
    if not t or not v or abs(time.time() - int(t.group(1))) > tolerance:
        return False
    expected = hmac.new(secret.encode(), t.group(1).encode() + b'.' + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v.group(1))


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get('Content-Length', 0)))
        if not verify(os.environ['INVEXQR_WEBHOOK_SECRET'], body, self.headers.get('InvexQR-Signature', '')):
            self.send_response(400); self.end_headers(); return
        event = json.loads(body)  # event['id'], event['type'], event['data']
        print(json.dumps({'id': event['id'], 'type': event['type']}), flush=True)
        self.send_response(204); self.end_headers()

    def log_message(self, *args):
        pass


if __name__ == '__main__':
    HTTPServer(('127.0.0.1', int(os.environ.get('PORT', '3000'))), Handler).serve_forever()

Style des QR

Le champ style (création ou modification) reprend les réglages du studio : fg, bg (couleurs #RRGGBB), fg2 + gradient (dégradé linéaire ou radial), module (square, rounded, dots, diamond, vertical), eye (square, rounded, circle), eyeColor, ecc (L, M, Q, H), margin, logo (image data:image/png;base64,…, 300 Ko), logoSize, frame (none, bottom, box), frameText, frameColor, frameTextColor. Les options avancées (dégradé, logo, formes autres que carré et arrondi) suivent la formule. L’image (GET /qr/{id}/image) applique ce style en PNG ou SVG.

Conversions sans clé d’API

Pour une campagne avec suivi des conversions (formule Pro et plus), chaque scan ajoute iqr=… à l’adresse de destination. Quand le visiteur achète, votre serveur appelle l’adresse de conversion de la campagne (conversion_url), sans clé :

curl "https://go.invexqr.com/cv/<jeton-campagne>?iqr=<valeur-reçue>&valeur=15000&format=json"

Avec une clé d’API, utilisez plutôt POST /api/v1/conversions. L’extension WordPress / WooCommerce fait tout cela automatiquement.

Make, n8n, Zapier

Principe commun : un webhook InvexQR déclenche votre scénario (scan, conversion, coupon validé…), une étape vérifie la signature, puis vos actions s’enchaînent. Pour agir sur InvexQR, appelez l’API avec l’en-tête Authorization: Bearer iqr_live_….

n8n

  1. Téléchargez le scénario (Intégrations → Extensions) puis, dans n8n : Workflows → Import from File.
  2. Ouvrez le nœud « Secret InvexQR » et collez le secret whsec_… du webhook.
  3. Activez le scénario, copiez l’URL de production du nœud « Webhook InvexQR » et déclarez-la dans Intégrations → Webhooks.
  4. « Envoyer un test » : l’exécution test.ping apparaît dans n8n. Ajoutez vos actions derrière les sorties « scan », « conversion » et « coupon ».

Make

  1. Module Webhooks → Custom webhook, options Get request headers et JSON pass-through activées ; déclarez son adresse dans InvexQR puis « Envoyer un test ».
  2. Filtre : sha256(t + "." + corps ; "hex" ; secret) doit être égal à la valeur v1 de l’en-tête InvexQR-Signature.
  3. Actions : vos applications, ou HTTP → Make a request vers https://invexqr.com/api/v1 avec l’en-tête d’autorisation.

Zapier

  1. Déclencheur Webhooks by Zapier → Catch Raw Hook ; déclarez son adresse dans InvexQR puis « Envoyer un test ».
  2. Étape Code by Zapier (JavaScript) avec ce code, puis un Filter « valid est vrai » :
// Étape « Code by Zapier » (JavaScript), juste après le déclencheur « Webhooks by Zapier → Catch Raw Hook ».
// Champs d'entrée (Input Data) à mapper :
//   raw_body  → « Raw Body » du déclencheur
//   signature → « Headers Http Invexqr Signature » du déclencheur
//   secret    → votre secret de webhook InvexQR (whsec_…)
// Sortie : valid (true/false), type, event_id, et les données de l'événement à plat pour les étapes suivantes.
const crypto = require('crypto');

function verify(secret, body, header, toleranceSec = 300) {
  const t = /(?:^|,)\s*t=(\d+)/.exec(header || '');
  const v = /(?:^|,)\s*v1=([a-f0-9]{64})/.exec(header || '');
  if (!t || !v || Math.abs(Date.now() / 1000 - Number(t[1])) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(t[1] + '.' + body).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v[1]));
}

const valid = verify(inputData.secret, inputData.raw_body, inputData.signature);
const event = valid ? JSON.parse(inputData.raw_body) : {};
output = {
  valid,
  type: event.type || '',
  event_id: event.id || '',
  qr_name: (event.data && event.data.qr && event.data.qr.name) || '',
  campaign_id: (event.data && event.data.qr && event.data.qr.campaign_id) || (event.data && event.data.conversion && event.data.conversion.campaign_id) || '',
  value: (event.data && event.data.conversion && event.data.conversion.value) || 0,
  data_json: JSON.stringify(event.data || {}),
};

Le code de vérification (Zapier, PHP, Node.js, Python) et le scénario n8n sont testés avec de vraies signatures InvexQR. Les écrans de Make et Zapier sont décrits d’après leur documentation et peuvent évoluer.