API InvexQR
Version 2026-10 · REST + JSON · réservée à la formule Business · fichier OpenAPI 3.1
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"
}
}
| 400 | invalid_json, invalid_idempotency_key |
| 401 | authentication_required, invalid_api_key, api_key_revoked, api_key_expired |
| 403 | plan_required, insufficient_scope, quota_exceeded, organization_suspended, forbidden |
| 404 | not_found — y compris pour une ressource d’une autre organisation |
| 409 / 415 / 413 | conflict, idempotency_in_progress, unsupported_media_type, payload_too_large |
| 422 | validation_failed, idempotency_key_reused, format_not_supported, invalid_scan_token… |
| 429 | rate_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": …}, avecmetapour 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ètre | Où | Type | Description |
|---|---|---|---|
page | URL | integer | Page (à partir de 1). |
per_page | URL | integer | Éléments par page (200 au maximum). |
kind | URL | string (static, redirect, multilink, file, coupon) | Type de QR. |
status | URL | string (active, paused, suspended) | Statut. |
campaign_id | URL | integer | QR associés en ce moment à une campagne (supports et QR existants). |
batch_id | URL | integer | Codes d’un lot créé en masse. |
search | URL | string | Recherche 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ètre | Où | Type | Description |
|---|---|---|---|
Idempotency-Key | en-tête | string | Identifiant 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
| Champ | Type | Description |
|---|---|---|
kind | string (redirect, static) | |
name | string | |
destination | string | Obligatoire pour kind=redirect (https:// ajouté si absent). |
content | string | Obligatoire pour kind=static (ex. WIFI:T:WPA;S:Boutique;P:motdepasse;;). |
data_type | string | Type de contenu statique (texte libre, informatif). |
style | object | Style graphique (voir la page Développeurs). |
domain | string | Nom d’hôte d’un domaine personnalisé actif, ou « platform ». Défaut : domaine par défaut de l’organisation. |
expires_at | string | null | |
expired_url | string | |
app_links | object | kind=redirect : iPhone vers « ios » (App Store), Android vers « android » (Google Play), autres appareils vers « destination ». |
campaign_id | integer | |
channel | string (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_cost | integer |
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ètre | Où | Type | Description |
|---|---|---|---|
Idempotency-Key | en-tête | string | Identifiant 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
| Champ | Type | Description |
|---|---|---|
mode | string (dynamic, static) | |
name | string | |
campaign_id | integer | |
domain | string | |
items requis | array |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
Champs du corps
| Champ | Type | Description |
|---|---|---|
name | string | |
destination | string | |
status | string (active, paused) | |
style | object | Style graphique (voir la page Développeurs). |
expires_at | string | null | |
expired_url | string | |
app_links | object |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
format | URL | string (png, svg) | Format de l’image. |
size | URL | integer | Largeur en pixels (64 à 2000). |
margin | URL | integer | Marge en modules (0 à 10). |
ecc | URL | string (L, M, Q, H) | Niveau de correction d’erreur minimal. |
frame | URL | string (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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
from | URL | string | Premier jour (AAAA-MM-JJ, fuseau de l’organisation). Défaut : il y a 29 jours. |
to | URL | string | Dernier 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ètre | Où | Type | Description |
|---|---|---|---|
from | URL | string | Premier jour (AAAA-MM-JJ, fuseau de l’organisation). Défaut : il y a 29 jours. |
to | URL | string | Dernier jour inclus (AAAA-MM-JJ). Défaut : aujourd’hui. |
qr_id | URL | integer | Limiter à un QR. |
campaign_id | URL | integer | Limiter 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ètre | Où | Type | Description |
|---|---|---|---|
from | URL | string | Premier jour (AAAA-MM-JJ, fuseau de l’organisation). Défaut : il y a 29 jours. |
to | URL | string | Dernier jour inclus (AAAA-MM-JJ). Défaut : aujourd’hui. |
qr_id | URL | integer | Limiter à un QR. |
campaign_id | URL | integer | Limiter 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ètre | Où | Type | Description |
|---|---|---|---|
from | URL | string | Premier jour (AAAA-MM-JJ, fuseau de l’organisation). Défaut : il y a 29 jours. |
to | URL | string | Dernier jour inclus (AAAA-MM-JJ). Défaut : aujourd’hui. |
type | URL | string (scan, visit, click) | Type. |
status | URL | string (valid, bot, preview, prefetch, duplicate, rate, all) | Classification (`all` pour tout). |
qr_id | URL | integer | Limiter à un QR. |
after_id | URL | integer | Curseur. |
limit | URL | integer | Taille de page (1000 au maximum). |
Réponses : 200 · 401 · 403 · 429 · 422
MultiLink
POST /api/v1/multilink
Créer une page MultiLink
Crée la page et son QR code. links est facultatif ; chaque lien a un type (whatsapp, facebook, instagram, tiktok, site, telephone…) et une value en saisie simple (numéro, @pseudo, adresse).
Permission requise : qr:write.
| Paramètre | Où | Type | Description |
|---|---|---|---|
Idempotency-Key | en-tête | string | Identifiant 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
| Champ | Type | Description |
|---|---|---|
title requis | string | |
bio | string | |
theme | string | |
sector | string | |
domain | string | |
links | array |
curl -X POST https://invexqr.com/api/v1/multilink \
-H "Authorization: Bearer $INVEXQR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Boutique Ndolo","bio":"Mode et accessoires à Douala","theme":"classique","links":[{"type":"whatsapp","value":"+237 655 44 18 48"},{"type":"instagram","value":"@boutiquendolo"},{"type":"website","value":"boutique-ndolo.cm"}]}'
Réponses : 201 · 401 · 403 · 429 · 400 · 422
GET /api/v1/multilink/{id}
Détail d’une page MultiLink
Permission requise : qr:read.
| Paramètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
Réponses : 200 · 401 · 403 · 429 · 404
PATCH /api/v1/multilink/{id}
Modifier une page MultiLink
links remplace la liste complète dans l’ordre fourni. Indiquez l’id d’un lien existant pour le conserver : ses statistiques et son adresse de clic sont préservées.
Permission requise : qr:write.
| Paramètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
Champs du corps
| Champ | Type | Description |
|---|---|---|
title | string | |
bio | string | |
theme | string | |
accent | string | |
links | array |
Réponses : 200 · 401 · 403 · 429 · 400 · 404 · 422
Campagnes
GET /api/v1/campaigns
Lister les campagnes
Permission requise : campaigns:read.
| Paramètre | Où | Type | Description |
|---|---|---|---|
page | URL | integer | Page (à partir de 1). |
per_page | URL | integer | Éléments par page (200 au maximum). |
status | URL | string (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ètre | Où | Type | Description |
|---|---|---|---|
Idempotency-Key | en-tête | string | Identifiant 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
| Champ | Type | Description |
|---|---|---|
name requis | string | |
description | string | |
objective | string (notoriete, trafic, prospects, ventes, fidelisation, evenement) | |
owner_name | string | |
start_date | string | |
end_date | string | |
budget | integer | |
audience | string | |
destination requis | string | |
utm_enabled | boolean | |
conversion_goal | string | |
conversion_tracking | boolean |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
from | URL | string | Premier jour (AAAA-MM-JJ, fuseau de l’organisation). Défaut : il y a 29 jours. |
to | URL | string | Dernier 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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
Idempotency-Key | en-tête | string | Identifiant 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
| Champ | Type | Description |
|---|---|---|
channel requis | string (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) | |
label | string | |
destination | string | Adresse propre au support (sinon celle de la campagne). |
cost | integer | Coût du support en FCFA (impression, emplacement). |
domain | string |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
state | URL | string (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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
Idempotency-Key | en-tête | string | Identifiant 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
| Champ | Type | Description |
|---|---|---|
qr_ids requis | array | |
counted_from | string | `today`, `start` ou AAAA-MM-JJ (entre le début de la campagne et aujourd’hui). |
pass_token | boolean | Transmettre le jeton iqr à la destination (QR de redirection, campagne avec suivi des conversions). |
cost | integer | Coû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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
qr | chemin | integer | Identifiant du QR |
Champs du corps
| Champ | Type | Description |
|---|---|---|
cost | integer | null | |
pass_token | boolean |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
qr | chemin | integer | Identifiant du QR |
void | URL | boolean | Annuler 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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
Idempotency-Key | en-tête | string | Identifiant 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
| Champ | Type | Description |
|---|---|---|
date | string | |
quantity | integer | |
value | integer | Montant en FCFA. |
qr_id | integer | |
note | string |
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
| Champ | Type | Description |
|---|---|---|
scan_token requis | string | |
value | integer | |
quantity | integer | |
campaign_id | integer | Né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ètre | Où | Type | Description |
|---|---|---|---|
Idempotency-Key | en-tête | string | Identifiant 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
| Champ | Type | Description |
|---|---|---|
url requis | string | |
events requis | objet | |
description | string |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
Champs du corps
| Champ | Type | Description |
|---|---|---|
url | string | |
events | array | |
description | string | |
status | string (active, disabled) |
Réponses : 200 · 401 · 403 · 429 · 400 · 404 · 422
DELETE /api/v1/webhooks/{id}
Supprimer un webhook
Permission requise : webhooks:manage.
| Paramètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
Réponses : 200 · 401 · 403 · 429 · 404
GET /api/v1/webhooks/{id}/deliveries
Journal des livraisons
Conservé 30 jours.
Permission requise : webhooks:manage.
| Paramètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
page | URL | integer | Page (à partir de 1). |
per_page | URL | integer | Éléments par page (200 au maximum). |
status | URL | string (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ètre | Où | Type | Description |
|---|---|---|---|
id | chemin | integer | Identifiant |
did | chemin | integer | Identifiant 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énement | Déclenché quand |
|---|---|
scan.recorded | Scan ou visite comptabilisé (robots et doublons exclus) |
click.recorded | Clic sur un lien MultiLink |
conversion.recorded | Conversion enregistrée (mesurée ou déclarée) |
coupon.issued | Code de coupon délivré à un client |
coupon.redeemed | Coupon validé en caisse |
qr.created | QR code créé |
qr.updated | QR code modifié (destination, statut, nom) |
qr.deleted | QR code supprimé |
batch.created | Lot de QR codes créé en masse |
campaign.created | Campagne créée |
campaign.updated | Campagne modifiée |
campaign.qr_attached | QR codes existants associés à une campagne |
campaign.qr_detached | QR code retiré d’une campagne (le QR n’est pas supprimé) |
alert.triggered | Alerte de performance déclenchée |
test.ping | Bouton « 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
- Téléchargez le scénario (Intégrations → Extensions) puis, dans n8n : Workflows → Import from File.
- Ouvrez le nœud « Secret InvexQR » et collez le secret
whsec_…du webhook. - Activez le scénario, copiez l’URL de production du nœud « Webhook InvexQR » et déclarez-la dans Intégrations → Webhooks.
- « Envoyer un test » : l’exécution
test.pingapparaît dans n8n. Ajoutez vos actions derrière les sorties « scan », « conversion » et « coupon ».
Make
- Module Webhooks → Custom webhook, options Get request headers et JSON pass-through activées ; déclarez son adresse dans InvexQR puis « Envoyer un test ».
- Filtre :
sha256(t + "." + corps ; "hex" ; secret)doit être égal à la valeurv1de l’en-têteInvexQR-Signature. - Actions : vos applications, ou HTTP → Make a request vers
https://invexqr.com/api/v1avec l’en-tête d’autorisation.
Zapier
- Déclencheur Webhooks by Zapier → Catch Raw Hook ; déclarez son adresse dans InvexQR puis « Envoyer un test ».
- É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.