Findings prod-testés + fallbacks ISV-only pour /platforms/v1 403. Breaking change sur la signature IsvWebhooks::create(). PHP 8.2+.
GitHub Release v1.6.0 →
Nouveaux helpers IsvAccounts::getOnboardingUrl(), isVerified(), isAcquiringEnabled() —
Répliquent les méthodes ConnectedAccounts via /isv/v1/ qui fonctionne sur les comptes ISV pure (vs /platforms/v1/ qui retourne 403 sans Marketplace API activée).
Fix breaking sur IsvWebhooks::create() —
Signature passée de $eventType:string à $eventTypeId:int. L'API Viva exige l'ID numérique (1796/1797/1798/1799/8193/8194), pas un nom d'event en string.
Nouvelle méthode IsvWebhooks::verificationToken() —
GET /isv/v1/webhooks/token pour récupérer la verification key requise avant tout enregistrement. Sert à signer les webhooks entrants HMAC-SHA256.
MerchantWebhookRegistrar — nouveau endpoint_unavailable statut —
Distingue le HTTP 404 sur /api/messages/config (déprécié/restreint sur la plupart des comptes ISV en 2026) des autres échecs. Plus 3 helpers statiques : allSucceeded(), hasEndpointIssue(), allFailed().
Référence complète skill/references/prod-findings.md —
Référence prod-testée des endpoints qui marchent / ne marchent PAS sur un vrai compte ISV, split OAuth ISV vs Smart Checkout, pièges UX onboarding, handshake signature webhook, tickets à ouvrir chez le support Viva.
Doc enrichie sur ConnectedAccounts —
Avertissement HTTP 403 sur prod ISV + table de mapping vers IsvAccounts. Pareil pour IsvMessages (HTTP 404 + polling fallback), IsvAccounts::list() et IsvWebhooks::list() (HTTP 405 — tracer côté app).
📜 Antérieur — Quoi de neuf en v1.5.0 (1er mai 2026)
Nouvelle ressource IsvMessages —
Enregistrement de webhooks au niveau marchand via Composite Basic Auth (/api/messages/config).
Nouveau helper MerchantWebhookRegistrar —
Enregistrement idempotent des événements banking pour un marchand connecté.
Constante BANKING_EVENTS = [768, 769, 2054]
IsvConfig::isProduction() et isSandbox()
Installation
composer require qrcommunication/viva-isv-sdk
Prérequis : PHP 8.2+ avec les extensions ext-json et ext-curl.
Credentials requis — où les trouver dans le Dashboard Viva Wallet
Credential
Emplacement dans le Dashboard
clientId
Settings > API Access > ISV OAuth Credentials > Client ID
Transactions des marchands connectés (capture, récurrent, annulation)
Composite Basic Auth — format non documenté par Viva Wallet
Le format {ResellerID}:{ConnectedMerchantID} comme username avec {ResellerAPIKey} comme password a été découvert empiriquement lors de la certification ISV. Le SDK construit ce header automatiquement — vous passez uniquement le connectedMerchantId aux méthodes de $isv->transactions.
Token OAuth2 — refresh automatique
Le token Bearer est obtenu automatiquement et rafraîchi avant expiration (marge de 60 secondes). Pour forcer un refresh manuel :
$isv->invalidateToken();
Référence des ressources
1
ConnectedAccounts
$isv->accounts
Gestion des comptes marchands connectés à la plateforme ISV. Création, consultation, onboarding KYB, vérification, mise à jour et suppression.
// Créer un compte connecté avec branding
$account = $isv->accounts->create(
email: 'merchant@example.com',
returnUrl: 'https://myapp.com/onboarding/complete',
partnerName: 'Ma Plateforme',
logoUrl: 'https://myapp.com/logo.png',
);
// Rediriger le marchand vers l'onboarding KYB
header('Location: ' . $account['invitation']['redirectUrl']);
// Vérifier le statut KYB
if ($isv->accounts->isVerified($account['accountId'])) {
echo 'Le marchand peut recevoir des paiements';
}
// Récupérer l'URL d'onboarding (null si déjà vérifié)
$url = $isv->accounts->onboardingUrl($account['accountId']);
// Lister tous les comptes connectés
$accounts = $isv->accounts->list();
// Consulter un compte
$details = $isv->accounts->get($account['accountId']);
// Mettre à jour
$isv->accounts->update($account['accountId'], ['email' => 'new@example.com']);
// Supprimer
$isv->accounts->delete($account['accountId']);
2
IsvAccounts
$isv->isvAccounts
Comptes ISV via le namespace /isv/v1/ avec options de branding personnalisé (couleur primaire, logo).
Utiliser IsvAccounts sur les comptes ISV pure —
L'API Marketplace (/platforms/v1/*) retourne HTTP 403 sauf si explicitement activée par Viva. Les méthodes getOnboardingUrl(), isVerified() et isAcquiringEnabled() (v1.6) répliquent les méthodes ConnectedAccounts via /isv/v1/ qui fonctionne toujours sur les comptes ISV.
Création d'ordres de paiement Smart Checkout pour les marchands connectés avec commission ISV.
Méthode
Signature
Retour
create
create(string $connectedMerchantId, int $amount, int $isvAmount, ?string $customerDescription, ?string $merchantReference, bool $allowRecurring, bool $preauth)
array{order_code, checkout_url}
checkoutUrl
checkoutUrl(int $orderCode)
string
$order = $isv->orders->create(
connectedMerchantId: 'merchant-uuid',
amount: 1500, // 15,00 EUR
isvAmount: 100, // 1,00 EUR de commission
customerDescription: 'Consultation bien-être',
merchantReference: 'INV-2026-001',
allowRecurring: true, // Tokeniser la carte pour les récurrents
preauth: false,
);
echo $order['order_code']; // 1234567890
echo $order['checkout_url']; // https://demo.vivapayments.com/web/checkout?ref=...
// Reconstruire l'URL à partir d'un code existant
$url = $isv->orders->checkoutUrl(1234567890);
Règles isvAmount :
isvAmount doit être <= amount — sinon InvalidArgumentException.
Le marchand connecté utilise sa source de paiement par défaut — ne jamais passer de sourceCode.
4
IsvTransactions
$isv->transactions
Opérations sur les transactions des marchands connectés. Le SDK utilise automatiquement le Composite Basic Auth pour tous les appels de cette ressource.
Breaking change v1.6 sur create() —
Signature passée de $eventType:string à $eventTypeId:int. L'API Viva rejette les noms d'event en string — seul l'ID numérique fonctionne (1796, 1797, 1798, 1799, 8193, 8194). Migration : remplacer 'transaction.payment.created' par 1796.
Handshake de verification obligatoire —
Avant le 1er appel à create(), récupérer la verification key via verificationToken() et faire répondre votre URL webhook par {"Key": "<clé>"} sur les requêtes GET. Sans cela Viva refuse l'enregistrement. Utiliser la même clé pour vérifier les signatures HMAC-SHA256 des webhooks entrants.
// 1. Récupérer la verification key (handshake obligatoire)
$key = $isv->isvWebhooks->verificationToken()['Key'];
// → Persister app-side, utiliser pour signer les webhooks entrants
// Configurer endpoint GET pour répondre {"Key": "$key"}
// 2. Créer un webhook (numeric eventTypeId, pas string)
$webhook = $isv->isvWebhooks->create(
url: 'https://myapp.com/webhooks/viva',
eventTypeId: 1796, // Transaction Payment Created
);
// Events ISV-level supportés :
// 1796 = Transaction Payment Created
// 1797 = Transaction Reversal Created (refund)
// 1798 = Transaction Failed
// 1799 = Transaction Price Calculated
// 8193 = Account Connected (KYB completion)
// 8194 = Account Verification Status Changed
foreach ([1796, 1797, 1798, 1799, 8193, 8194] as $eventId) {
$isv->isvWebhooks->create($url, $eventId);
}
// ⚠ list() retourne HTTP 405 en prod — tracer registered EventTypeIds côté app
// Modifier
$isv->isvWebhooks->update(
webhookId: $webhook['webhookId'],
url: 'https://myapp.com/webhooks/viva-v2',
eventTypeId: 1797,
);
// Supprimer
$isv->isvWebhooks->delete($webhook['webhookId']);
10
Webhooks
$isv->webhooks
Vérification de la requête GET initiale de Viva Wallet et parsing des payloads POST (21 types d'événements).
// GET — vérification initiale
$verificationKey = config('services.viva.verification_key');
return response()->json(
$isv->webhooks->verificationResponse($verificationKey)
);
// => {"StatusCode": 0, "Key": "votre-cle"}
// POST — parsing des événements
$event = $isv->webhooks->parse(file_get_contents('php://input'));
echo $event['event_type']; // 'transaction.payment.created'
echo $event['event_type_id']; // 1796
$data = $event['event_data'];
// Pattern Laravel Controller
match ($event['event_type']) {
'transaction.payment.created' => $this->handlePayment($event['event_data']),
'transaction.refund.created' => $this->handleRefund($event['event_data']),
'account.connected' => $this->handleNewAccount($event['event_data']),
default => null,
};
// Vérifier si un eventTypeId est connu (méthode statique)
use QrCommunication\VivaIsv\Resources\Webhooks;
if (Webhooks::isKnownEvent(1796)) {
echo 'Événement reconnu';
}
11
IsvMessages
$isv->isvMessagesNew v1.5.0
Enregistrement d'abonnements webhook au niveau marchand via /api/messages/config. Utilise le Composite Basic Auth automatiquement — vous passez uniquement le connectedMerchantId.
Production gotcha (2026-05) : /api/messages/config retourne HTTP 404 —
Cet endpoint semble déprécié/restreint sur la plupart des marchands ISV-managed. Tous les appels register/list/delete jettent ApiException 404. Workaround : poller les settlements via wallet API sur un job de reconciliation périodique. Les événements ne sont pas perdus — ils sont persistés dans le ledger wallet Viva.
Méthode
Signature
Retour
register
register(string $connectedMerchantId, string $callbackUrl, int $eventTypeId)
array
list
list(string $connectedMerchantId)
array
delete
delete(string $connectedMerchantId, int $eventTypeId)
array
use QrCommunication\VivaIsv\Helpers\MerchantWebhookRegistrar;
// Enregistrer un seul événement banking pour un marchand connecté
$isv->isvMessages->register(
connectedMerchantId: 'merchant-uuid',
callbackUrl: 'https://myapp.com/api/webhooks/viva',
eventTypeId: 768, // Command Bank Transfer Created
);
// Lister les souscriptions actives du marchand
$subscriptions = $isv->isvMessages->list('merchant-uuid');
// Supprimer un abonnement
$isv->isvMessages->delete('merchant-uuid', 768);
Helper idempotent qui enregistre tous les événements banking pour un marchand connecté en un seul appel. Ignore les événements déjà enregistrés. Utilise la constante BANKING_EVENTS = [768, 769, 2054] par défaut.
4 statuts depuis v1.6 —
Les entrées de résultat ont 4 statuts distincts : created (succès), already_exists (duplicate idempotent), endpoint_unavailable (HTTP 404 — API Viva dépréciée/restreinte), failed (autre erreur). Les 3 helpers statiques permettent de brancher proprement sans parser le tableau manuellement.
use QrCommunication\VivaIsv\Helpers\MerchantWebhookRegistrar;
// Enregistrer les 3 événements banking en un seul appel
$results = $isv->merchantWebhookRegistrar()->registerAll(
connectedMerchantId: $merchantId,
callbackUrl: 'https://app.example.com/api/webhooks/viva',
);
// Format v1.6 — chaque entrée :
// ['event_id' => 768, 'status' => 'created'|'already_exists'|'endpoint_unavailable'|'failed', 'message' => '...']
// Branching propre via les helpers statiques v1.6
if (MerchantWebhookRegistrar::allSucceeded($results)) {
// Tout est OK (created OU already_exists)
} elseif (MerchantWebhookRegistrar::hasEndpointIssue($results)) {
// L'API merchant-webhooks n'est pas dispo sur ce compte ISV (HTTP 404).
// Fallback : job de reconciliation périodique via wallet API.
Log::info('Viva merchant webhooks endpoint unavailable — using polling reconciliation');
} elseif (MerchantWebhookRegistrar::allFailed($results)) {
// Erreur réseau / auth — retry plus tard
}
// Enregistrer uniquement un sous-ensemble d'événements
$results = $isv->merchantWebhookRegistrar()->registerAll(
connectedMerchantId: $merchantId,
callbackUrl: 'https://app.example.com/api/webhooks/viva',
events: [768, 769],
);
// Consulter les IDs gérés par le helper
$bankingEvents = MerchantWebhookRegistrar::BANKING_EVENTS; // [768, 769, 2054]
Idempotent :
Peut être appelé plusieurs fois sans risque — les événements déjà enregistrés sont en `already_exists`, pas re-enregistrés. À utiliser dans votre flux d'onboarding après la vérification KYB du marchand.
Enums
EcrEventId — codes résultat Cloud Terminal
use QrCommunication\VivaIsv\Enums\EcrEventId;
$event = EcrEventId::tryFrom($session['eventId']);
$event->isSuccessful(); // true si SUCCESS (0)
$event->isTerminal(); // true si état final (pas IN_PROGRESS)
$event->shouldPoll(); // true si IN_PROGRESS (1100)
$event->label(); // 'Transaction successful', 'Declined', etc.
Valeur
Constante
Description
0
SUCCESS
Transaction réussie
1003
TERMINAL_TIMEOUT
Terminal hors délai
1006
DECLINED
Transaction refusée
1016
ABORTED
Transaction annulée
1020
INSUFFICIENT_FUNDS
Fonds insuffisants
1099
GENERIC_ERROR
Erreur générique
1100
IN_PROGRESS
En cours — continuer le polling
6000
BAD_PARAMS
Paramètres invalides
TransactionEventId — codes de déclin détaillés
use QrCommunication\VivaIsv\Enums\TransactionEventId;
$decline = TransactionEventId::tryFrom($session['transactionEventId']);
echo $decline->label(); // 'Insufficient funds'
echo $decline->testAmount(); // 9951 (montant pour déclencher ce déclin en demo)
Valeur
Constante
Montant test (centimes)
10001
REFER_TO_ISSUER
—
10003
INVALID_MERCHANT
—
10004
PICKUP_CARD
—
10005
DO_NOT_HONOR
—
10006
GENERAL_ERROR
9906
10012
INVALID_TRANSACTION
—
10013
INVALID_AMOUNT
—
10014
INVALID_CARD
9914
10030
FORMAT_ERROR
—
10041
LOST_CARD
—
10043
STOLEN_CARD
9920
10051
INSUFFICIENT_FUNDS
9951
10054
EXPIRED_CARD
9954
10055
INCORRECT_PIN
—
10057
NOT_PERMITTED_CARDHOLDER
9957
10058
NOT_PERMITTED_TERMINAL
—
10061
WITHDRAWAL_LIMIT
9961
10062
RESTRICTED_CARD
—
10063
SECURITY_VIOLATION
—
10065
ACTIVITY_LIMIT
—
10068
LATE_RESPONSE
—
10070
CALL_ISSUER
—
10075
PIN_TRIES_EXCEEDED
—
10200
UNMAPPED
—
Environment
use QrCommunication\VivaIsv\Enums\Environment;
// Passer en string ou en enum — les deux sont acceptés
$isv = new VivaIsvClient(..., environment: 'demo');
$isv = new VivaIsvClient(..., environment: Environment::PRODUCTION);
IsvConfig::isProduction() / isSandbox()New v1.5.0
Helpers booléens sur l'objet config — pratiques pour la logique conditionnelle sans comparaison de chaînes.
use QrCommunication\VivaIsv\IsvConfig;
$config = new IsvConfig(
clientId: 'isv-client-id.apps.vivapayments.com',
clientSecret: 'isv-client-secret',
merchantId: 'isv-merchant-uuid',
apiKey: 'isv-api-key',
resellerId: 'reseller-uuid',
resellerApiKey: 'reseller-api-key',
environment: 'production',
);
$config->isProduction(); // true
$config->isSandbox(); // false
// Utilisation dans une logique conditionnelle
if ($config->isProduction()) {
logger()->info('Exécution en production — utilisation des vrais credentials');
}
Gestion des erreurs
Le SDK définit 3 exceptions dans le namespace QrCommunication\VivaIsv\Exceptions :
Les méthodes capture() et recurring() lancent ApiException si ErrorCode !== 0 dans la réponse Viva Wallet.
Événements webhook (24)
ID
Type
1796
transaction.payment.created
1797
transaction.refund.created
1798
transaction.payment.cancelled
1799
transaction.reversal.created
1800
transaction.preauth.created
1801
transaction.preauth.completed
1802
transaction.preauth.cancelled
1810
pos.session.created
1811
pos.session.failed
1812
transaction.price.calculated
1813
transaction.failed
1819
account.connected
1820
account.verification.status.changed
1821
account.transaction.created
1822
command.bank.transfer.created
1823
command.bank.transfer.executed
1824
transfer.created
1825
obligation.created
1826
obligation.captured
1827
order.updated
1828
sale.transactions.file
Événements banking — niveau marchand (v1.5.0)
New
Ces 3 événements s'enregistrent par marchand connecté via /api/messages/config (pas via /isv/v1/webhooks). Utiliser le helper MerchantWebhookRegistrar pour les enregistrer en un seul appel.
ID
Type
Description
768
command.bank.transfer.created
Ordre de virement bancaire créé
769
command.bank.transfer.executed
Ordre de virement bancaire exécuté
2054
account.transaction.created
Transaction de compte (wallet) créée
Constante BANKING_EVENTS :
Le SDK expose
MerchantWebhookRegistrar::BANKING_EVENTS = [768, 769, 2054]
pour éviter de coder ces IDs en dur dans votre application.
Test en sandbox
Utiliser environment: 'demo' pour tester sans transactions réelles.
Carte de test
Champ
Valeur
Numéro
4111 1111 1111 1111
Expiration
Toute date future
CVV
111
Montants qui déclenchent des déclin en mode demo
use QrCommunication\VivaIsv\Enums\TransactionEventId;
// Obtenir le montant de test pour un type de déclin
$amount = TransactionEventId::INSUFFICIENT_FUNDS->testAmount(); // 9951
$order = $isv->orders->create('merchant-uuid', $amount);
Montant (centimes)
Déclin déclenché
9951
Fonds insuffisants (INSUFFICIENT_FUNDS)
9954
Carte expirée (EXPIRED_CARD)
9920
Carte volée (STOLEN_CARD)
9957
Carte non autorisée (NOT_PERMITTED_CARDHOLDER)
9961
Limite de retrait (WITHDRAWAL_LIMIT)
9906
Erreur générale (GENERAL_ERROR)
9914
Carte invalide (INVALID_CARD)
Pièges en production (testés 2026-05)
v1.6.0
Ces comportements ont été observés sur un vrai compte ISV production (Merchant 0119432c-...) lors d'une session de debug. Ils ne sont PAS dans la doc Viva officielle.
Endpoints qui ne fonctionnent PAS sur les comptes ISV pure
Endpoint
HTTP Status
Workaround
GET /isv/v1/accounts (isvAccounts->list())
405
Tracer les accountIds côté app
DELETE /isv/v1/accounts/{id}
405
Pas de cleanup possible
GET /isv/v1/webhooks (isvWebhooks->list())
405
Tracer les EventTypeIds côté app
/platforms/v1/accounts/* (Marketplace API)
403
Utiliser IsvAccounts (/isv/v1/) à la place
POST /api/messages/config (isvMessages->register())
404
Reconciliation par polling via wallet API
POST /api/sources (composite auth)
400
Pas besoin (Source par défaut Viva)
OAuth split — ISV vs Smart Checkout (obligatoire)
Les credentials ISV ne supportent que le grant client_credentials. Le flow Authorization Code (login OAuth utilisateur final pour connecter un compte business existant) requiert des credentials Smart Checkout séparées créées dans Viva → Settings → API Access → Smart Checkout. Tenter /connect/authorize avec les credentials ISV redirige toujours vers accounts.vivapayments.com/home/error?errorId=CfDJ8... (errorId chiffré .NET DataProtection — impossible à décoder).
Pièges UX d'onboarding
Email d'invitation déjà lié à un business Viva —
L'écran d'invitation Viva affiche un sélecteur "Connect existing" / "Create new business account". Cliquer "Connect existing" plante avec "Failed to connect with PartnerName" si le redirect URI Smart Checkout n'est pas whitelisté côté Viva. Forcer un KYB propre en utilisant un email frais (ex: alias +tag).
accountId peut finir lié à un autre email business —
Cas observé : invitation pour userA@example.com → écran sélecteur → Continue → erreur OAuth → l'accountId côté Viva finit lié à userB@otherdomain.com (multi-business Viva owner). Toujours vérifier merchantId et legalName après KYB avant de considérer l'onboarding réussi.
Pings sans signature —
Viva fait des appels périodiques (au moins toutes les heures) sur l'URL webhook SANS X-Viva-Signature depuis IPs Azure (51.138.x.x, 20.54.x.x). Renvoyer 200 sur ces calls (logger en warning) — sinon Viva considère le webhook comme cassé et le désactive.
Tickets support à ouvrir chez Viva
Item
Demande
Activation rôle ISV
"Activate ISV Partner Program on merchant {ID}"
App Smart Checkout séparée
"Create Smart Checkout OAuth app for our platform"
Whitelist redirect URI
"Add https://app.example.com/callback to redirect URIs of OAuth app {client_id}"
Activation /api/messages/config
"Enable merchant-level webhook registration on our ISV (events 768/769/2054)" — souvent refusé
Activation /platforms/v1/*
"Enable Marketplace API on our ISV" — souvent refusé