"> Aller au contenu principal
PHP PHP PHP v1.6.0 MIT

Viva Wallet ISV SDK

qrcommunication/viva-isv-sdk

PHP 8.2+, guzzlehttp/guzzle ^7.8

What's new in v1.6.0 May 2026

Production-tested findings + ISV-only fallbacks for /platforms/v1 403. Breaking change on IsvWebhooks::create() signature. PHP 8.2+. GitHub Release v1.6.0 →
  • New helpers IsvAccounts::getOnboardingUrl(), isVerified(), isAcquiringEnabled() — Replicate ConnectedAccounts methods on /isv/v1/ which works on pure-ISV accounts (vs /platforms/v1/ that returns 403 unless Marketplace API is enabled).
  • Breaking fix on IsvWebhooks::create() — Signature changed from $eventType:string to $eventTypeId:int. Viva's API requires the numeric ID (1796/1797/1798/1799/8193/8194), not a string event name.
  • New IsvWebhooks::verificationToken() — GET /isv/v1/webhooks/token to fetch the verification key required before any webhook registration. Used to sign incoming HMAC-SHA256 webhooks.
  • MerchantWebhookRegistrar — new endpoint_unavailable status — Distinguishes HTTP 404 on /api/messages/config (deprecated/restricted on most ISV accounts in 2026) from other failures. Plus 3 static helpers: allSucceeded(), hasEndpointIssue(), allFailed().
  • Comprehensive skill/references/prod-findings.md — Production-tested reference of endpoints that DO/DON'T work on a real ISV account, OAuth split between ISV and Smart Checkout credentials, onboarding UX gotchas, webhook signature handshake, support tickets to file with Viva.
  • Doc enriched on ConnectedAccounts — Warning HTTP 403 on prod ISV + mapping table to IsvAccounts equivalents. Same for IsvMessages (HTTP 404 + polling fallback), IsvAccounts::list() and IsvWebhooks::list() (both HTTP 405 — track app-side).
📜 Earlier — What's new in v1.5.0 (May 1, 2026)
  • New resource IsvMessages — Registration of merchant-level webhooks via Composite Basic Auth (/api/messages/config).
  • New helper MerchantWebhookRegistrar — Idempotent registration of banking events for a connected merchant.
  • Constant BANKING_EVENTS = [768, 769, 2054]
  • IsvConfig::isProduction() and isSandbox()

Installation

composer require qrcommunication/viva-isv-sdk

Requirements: PHP 8.2+ with extensions ext-json and ext-curl.

Required credentials — where to find them in the Viva Wallet Dashboard
Credential Location in Dashboard
clientIdSettings > API Access > ISV OAuth Credentials > Client ID
clientSecretSettings > API Access > ISV OAuth Credentials > Client Secret
merchantIdSettings > API Access > Merchant ID
apiKeySettings > API Access > API Key
resellerIdSettings > API Access > Reseller Credentials > Reseller ID
resellerApiKeySettings > API Access > Reseller Credentials > Reseller API Key

Quick Start

use QrCommunication\VivaIsv\VivaIsvClient;

// 1. Instancier le client avec les 6 credentials
$isv = new VivaIsvClient(
    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:    'demo', // 'demo' ou 'production'
);

// 2. Créer un compte marchand connecté
$account = $isv->accounts->create(
    email: 'merchant@example.com',
    returnUrl: 'https://myapp.com/onboarding/complete',
);
// => ['accountId' => 'uuid', 'invitation' => ['redirectUrl' => 'https://...']]

// 3. Créer un ordre avec commission ISV
$order = $isv->orders->create(
    connectedMerchantId: $account['accountId'],
    amount: 1500,    // 15,00 EUR
    isvAmount: 100,  // 1,00 EUR de commission
);
// => ['order_code' => 1234567890, 'checkout_url' => 'https://...']

// 4. Rediriger le client vers le checkout
header('Location: ' . $order['checkout_url']);

// 5. Capturer une pré-autorisation
$isv->transactions->capture('preauth-txn-uuid', 'merchant-uuid', amount: 1500);

// 6. Vente sur terminal POS
$session = $isv->terminals->sale(
    terminalId: 16014231,
    amount: 1500,
    isvAmount: 100,
    terminalMerchantId: 'merchant-uuid',
    cashRegisterId: 'POS-CR1',
);
$result = $isv->terminals->pollUntilComplete($session['session_id']);

// 7. Rembourser
$isv->transactions->cancel('txn-uuid', 'merchant-uuid', amount: 500);

// Test de connexion
if ($isv->testConnection()) {
    echo 'Connexion ISV OK';
}

Configuration

Constructor VivaIsvClient

Parameter Type Description
clientIdstringISV OAuth2 Client ID (*.apps.vivapayments.com)
clientSecretstringISV OAuth2 Client Secret
merchantIdstringISV Merchant UUID (for ISV Basic Auth)
apiKeystringISV API Key (for ISV Basic Auth)
resellerIdstringReseller UUID (for Composite Basic Auth)
resellerApiKeystringReseller API Key (for Composite Basic Auth)
environmentstring|Environment'demo' or 'production' (default: 'demo')

Resource architecture

VivaIsvClient
├── $accounts          → ConnectedAccounts   (7 methods)
├── $isvAccounts       → IsvAccounts         (3 methods)
├── $orders            → IsvOrders           (2 methods)
├── $transactions      → IsvTransactions     (5 methods)
├── $terminals         → EcrTerminals        (6 methods)
├── $transfers         → Transfers           (2 methods)
├── $marketplace       → MarketplaceOrders   (2 methods)
├── $nativeCheckout    → NativeCheckoutIsv   (2 methods)
├── $isvWebhooks       → IsvWebhooks             (4 methods)
├── $webhooks          → Webhooks                (3 methods)
├── $isvMessages       → IsvMessages             (3 methods — NEW v1.5.0)
└── merchantWebhookRegistrar() → MerchantWebhookRegistrar (helper — NEW v1.5.0)

39 methods total — 12 resources/helpers

Authentication

The SDK handles 3 authentication modes automatically depending on the operation. You only need to provide the 6 credentials to the constructor.

Mode Username Password Used for
ISV OAuth2 (Bearer) clientId clientSecret Accounts, ISV orders, terminals, transfers, marketplace, native checkout, ISV webhooks
ISV Basic Auth merchantId apiKey Legacy operations on the ISV own account
Composite Basic Auth {resellerId}:{connectedMerchantId} resellerApiKey Transactions of connected merchants (capture, recurring, cancellation)
Composite Basic Auth — undocumented format by Viva Wallet
The format {ResellerID}:{ConnectedMerchantID} as username with {ResellerAPIKey} as password was discovered empirically during ISV certification. The SDK builds this header automatically — you only pass the connectedMerchantId to the methods of $isv->transactions.

OAuth2 token — automatic refresh

The Bearer token is obtained automatically and refreshed before expiration (60-second margin). To force a manual refresh:

$isv->invalidateToken();

Resource Reference

1 ConnectedAccounts $isv->accounts

Management of merchant accounts connected to the ISV platform. Creation, retrieval, KYB onboarding, verification, update and deletion.

MethodSignatureReturn
createcreate(string $email, string $returnUrl, ?string $partnerName, ?string $logoUrl)array{accountId, invitation}
getget(string $accountId)array (account details)
listlist()array (paginated list)
isVerifiedisVerified(string $accountId)bool
onboardingUrlonboardingUrl(string $accountId)?string
updateupdate(string $accountId, array $attributes)array
deletedelete(string $accountId)array
// 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

ISV accounts via the /isv/v1/ namespace with custom branding options (primary color, logo).

MethodSignatureReturn
createcreate(string $email, string $returnUrl, ?string $partnerName, ?string $primaryColor, ?string $logoUrl)array{accountId, invitation}
getget(string $accountId)array
getOnboardingUrl v1.6getOnboardingUrl(string $accountId)?string
isVerified v1.6isVerified(string $accountId)bool
isAcquiringEnabled v1.6isAcquiringEnabled(string $accountId)bool
listlist()array ⚠ HTTP 405 prod
Use IsvAccounts on pure-ISV accounts — The Marketplace API (/platforms/v1/*) returns HTTP 403 unless explicitly enabled by Viva. The methods getOnboardingUrl(), isVerified() and isAcquiringEnabled() (v1.6) replicate ConnectedAccounts methods on /isv/v1/ which always works on ISV accounts.
$account = $isv->isvAccounts->create(
    email: 'merchant@example.com',
    returnUrl: 'https://myapp.com/onboarding/done',
    partnerName: 'Ma Plateforme',
    primaryColor: '#0052FF',
    logoUrl: 'https://myapp.com/logo.png',
);

$details = $isv->isvAccounts->get($account['accountId']);

// v1.6 — helpers prod-tested (toujours fonctionnels sur ISV)
$onboardingUrl = $isv->isvAccounts->getOnboardingUrl($account['accountId']);
$isVerified    = $isv->isvAccounts->isVerified($account['accountId']);
$canAcquire    = $isv->isvAccounts->isAcquiringEnabled($account['accountId']);

// ⚠ list() retourne HTTP 405 en prod — tracer accountIds côté app
// $all = $isv->isvAccounts->list(); // ApiException 405

3 IsvOrders $isv->orders

Creation of Smart Checkout payment orders for connected merchants with ISV commission.

MethodSignatureReturn
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);
Rules for isvAmount :
  • isvAmount must be <= amount — otherwise InvalidArgumentException.
  • The connected merchant uses their default payment source — never pass a sourceCode.

4 IsvTransactions $isv->transactions

Operations on transactions of connected merchants. The SDK automatically uses Composite Basic Auth for all calls to this resource.

MethodSignatureReturn
getget(string $transactionId, string $connectedMerchantId)array
listByDatelistByDate(string $connectedMerchantId, string $date)array
capturecapture(string $transactionId, string $connectedMerchantId, int $amount, ?int $isvAmount)array
recurringrecurring(string $initialTransactionId, string $connectedMerchantId, int $amount, ?int $isvAmount, ?string $sourceCode)array
cancelcancel(string $transactionId, string $connectedMerchantId, ?int $amount, ?string $sourceCode)array
// Consulter une transaction
$txn = $isv->transactions->get('txn-uuid', 'merchant-uuid');

// Lister les transactions d'une journée
$transactions = $isv->transactions->listByDate('merchant-uuid', '2026-03-18');

// Capturer une pré-autorisation
$isv->transactions->capture(
    transactionId: 'preauth-txn-uuid',
    connectedMerchantId: 'merchant-uuid',
    amount: 1500,
    isvAmount: 100,
);

// Paiement récurrent (à partir d'une transaction initiale tokenisée)
$isv->transactions->recurring(
    initialTransactionId: 'initial-txn-uuid',
    connectedMerchantId: 'merchant-uuid',
    amount: 1500,
    isvAmount: 100,
);

// Remboursement total
$isv->transactions->cancel('txn-uuid', 'merchant-uuid');

// Remboursement partiel (5,00 EUR)
$isv->transactions->cancel('txn-uuid', 'merchant-uuid', amount: 500);
Prerequisite: The option "Allow recurring payments and pre-auth captures via API" must be enabled in Settings > API Access of the ISV account.

5 EcrTerminals $isv->terminals

ISV Cloud POS payment terminals. Search, sale, session polling, abort.

MethodSignatureReturn
searchsearch(?string $merchantId, ?int $statusId, ?string $sourceCode)array
salesale(int $terminalId, int $amount, int $isvAmount, string $terminalMerchantId, string $cashRegisterId, ?string $merchantReference, int $currencyCode, ?string $sessionId)array{session_id, success}
getSessiongetSession(string $sessionId)array
listSessionslistSessions(string $date)array
abortabort(string $sessionId, string $cashRegisterId)array
pollUntilCompletepollUntilComplete(string $sessionId, int $timeoutSeconds, int $intervalMs)array
use QrCommunication\VivaIsv\Enums\EcrEventId;

// Rechercher les terminaux d'un marchand
$terminals = $isv->terminals->search(merchantId: 'merchant-uuid');

// Vente POS ISV
$session = $isv->terminals->sale(
    terminalId: 16014231,
    amount: 1500,
    isvAmount: 100,
    terminalMerchantId: 'merchant-uuid',
    cashRegisterId: 'PratiConnect-CR1',
    merchantReference: 'INV-2026-001',
);

echo $session['session_id']; // UUID de session

// Polling jusqu'au résultat (défaut : 120s timeout, 3s intervalle)
$result = $isv->terminals->pollUntilComplete($session['session_id']);

// Interpréter le résultat avec l'enum
$eventId = EcrEventId::tryFrom($result['eventId']);
if ($eventId?->isSuccessful()) {
    echo 'Transaction réussie : ' . $result['transactionId'];
} else {
    echo 'Échec : ' . $eventId?->label();
}

// Annuler une session active
$isv->terminals->abort('session-uuid', 'PratiConnect-CR1');

// Consulter une session
$session = $isv->terminals->getSession('session-uuid');

// Lister les sessions d'une journée
$sessions = $isv->terminals->listSessions('2026-03-18');
Important notes:
  • Pre-authorization is not supported via ISV Cloud Terminal — use Smart Checkout with preauth: true.
  • Abort uses GET (not DELETE) — an undocumented quirk of the Viva API. The SDK handles this automatically.
  • The sessionId is auto-generated if not provided.
  • The SDK builds isvDetails automatically.

6 Transfers $isv->transfers

Sending and reversing fund transfers to connected accounts.

MethodSignatureReturn
sendsend(string $targetAccountId, int $amount, ?string $sourceWalletId, ?string $transactionId, ?string $description)array{transferId}
reversereverse(string $transferId, ?int $amount)array{transferId}
// Envoyer des fonds à un vendeur
$transfer = $isv->transfers->send(
    targetAccountId: 'seller-account-uuid',
    amount: 1000,                    // 10,00 EUR
    transactionId: 'txn-uuid',       // Lier à une transaction existante
    description: 'Commission mars 2026',
);

echo $transfer['transferId'];

// Annuler totalement un transfert
$isv->transfers->reverse('transfer-uuid');

// Annuler partiellement (5,00 EUR)
$isv->transfers->reverse('transfer-uuid', amount: 500);

7 MarketplaceOrders $isv->marketplace

Marketplace orders with automatic transfer to the seller. The platform fee is the difference between amount and sellerAmount.

MethodSignatureReturn
create create(int $amount, string $sellerAccountId, int $sellerAmount, ?string $customerDescription, ?string $merchantReference, ?string $sourceCode, bool $preauth) array{order_code, checkout_url, platform_fee}
cancel cancel(string $transactionId, ?int $amount, bool $reverseTransfers, bool $refundPlatformFee) array
// Créer un ordre marketplace
$order = $isv->marketplace->create(
    amount: 1500,                    // 15,00 EUR total
    sellerAccountId: 'seller-uuid',
    sellerAmount: 1200,              // 12,00 EUR au vendeur
    customerDescription: 'Achat marketplace',
    merchantReference: 'MP-2026-001',
);

echo $order['order_code'];
echo $order['checkout_url'];
echo $order['platform_fee'];  // 300 = 3,00 EUR de commission plateforme

// Remboursement total avec reversal des transferts
$isv->marketplace->cancel('txn-uuid');

// Remboursement partiel sans rembourser la commission plateforme
$isv->marketplace->cancel(
    transactionId: 'txn-uuid',
    amount: 500,
    reverseTransfers: true,
    refundPlatformFee: false,
);

8 NativeCheckoutIsv $isv->nativeCheckout

Native server-to-server payment for connected merchants, without Smart Checkout redirection.

MethodSignatureReturn
createChargeToken createChargeToken(string $connectedMerchantId, int $amount, string $paymentData, int $paymentMethodId) array{chargeToken}
createTransaction createTransaction(string $connectedMerchantId, string $chargeToken, int $amount, int $isvAmount, int $currencyCode, ?string $merchantTrns, ?string $customerTrns, bool $preauth) array{transactionId, statusId}

2-step flow:

  1. The client collects card data via the Viva JS SDK and obtains paymentData (encrypted client-side).
  2. Your server creates a charge token via createChargeToken(), then executes the transaction via createTransaction().
// Étape 1 : créer un charge token
$token = $isv->nativeCheckout->createChargeToken(
    connectedMerchantId: 'merchant-uuid',
    amount: 1500,
    paymentData: $encryptedCardData,  // Du JS SDK Viva
    paymentMethodId: 0,               // 0 = carte par défaut
);

// Étape 2 : exécuter la transaction
$txn = $isv->nativeCheckout->createTransaction(
    connectedMerchantId: 'merchant-uuid',
    chargeToken: $token['chargeToken'],
    amount: 1500,
    isvAmount: 100,
    currencyCode: 978,                // EUR
    merchantTrns: 'INV-2026-001',
    customerTrns: 'Consultation bien-être',
);

echo $txn['transactionId'];
echo $txn['statusId'];

9 IsvWebhooks $isv->isvWebhooks

Creation, listing, update and deletion of ISV webhook subscriptions.

MethodSignatureReturn
verificationToken v1.6verificationToken()array{Key: string}
create v1.6 BREAKINGcreate(string $url, int $eventTypeId)array
listlist()array ⚠ HTTP 405 prod
updateupdate(string $webhookId, string $url, ?int $eventTypeId)array
deletedelete(string $webhookId)array
Breaking change v1.6 on create() — Signature changed from $eventType:string to $eventTypeId:int. Viva's API rejects string event names — only the numeric ID works (1796, 1797, 1798, 1799, 8193, 8194). Migration: replace 'transaction.payment.created' with 1796.
Verification handshake mandatory — Before calling create() the first time, fetch the verification key via verificationToken() and have your webhook URL respond {"Key": "<key>"} on GET requests. Without it Viva refuses registration. Use the same key for HMAC-SHA256 verification of incoming webhooks.
// 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

Verification of the initial Viva Wallet GET request and parsing of POST payloads (21 event types).

MethodSignatureReturn
verificationResponseverificationResponse(string $verificationKey)array{StatusCode, Key}
parseparse(string $rawBody)array{event_type, event_type_id, event_data}
isKnownEventWebhooks::isKnownEvent(int $eventTypeId) (static)bool
// 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->isvMessages New v1.5.0

Registration of merchant-level webhook subscriptions via /api/messages/config. Uses Composite Basic Auth automatically — you only pass the connectedMerchantId.

Production gotcha (2026-05): /api/messages/config returns HTTP 404 — This endpoint appears deprecated/restricted on most ISV-managed merchants. All register/list/delete calls throw ApiException 404. Workaround: poll settlements via wallet API on a periodic reconciliation job. The events themselves are not lost — they are persisted in Viva's wallet ledger.
MethodSignatureReturn
registerregister(string $connectedMerchantId, string $callbackUrl, int $eventTypeId)array
listlist(string $connectedMerchantId)array
deletedelete(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);

12 MerchantWebhookRegistrar $isv->merchantWebhookRegistrar() New v1.5.0

Idempotent helper that registers all banking events for a connected merchant in one call. Skips events already registered. Uses the constant BANKING_EVENTS = [768, 769, 2054] by default.

MethodSignatureReturn
registerAll registerAll(string $connectedMerchantId, string $callbackUrl, ?array $events = null) array<int, array{event_id, status, message?}>
allSucceeded v1.6 staticallSucceeded(array $results)bool
hasEndpointIssue v1.6 statichasEndpointIssue(array $results)bool
allFailed v1.6 staticallFailed(array $results)bool
4 statuses since v1.6 — The result entries now include 4 distinct statuses: created (success), already_exists (idempotent duplicate), endpoint_unavailable (HTTP 404 — Viva API deprecated/restricted), failed (other error). The 3 static helpers let you branch cleanly without parsing the array manually.
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: Safe to call multiple times — events already registered are listed as `already_exists`, not re-registered. Use this helper in your onboarding flow after merchant KYB verification.

Enums

EcrEventId — Cloud Terminal result codes

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.
ValueConstantDescription
0SUCCESSTransaction successful
1003TERMINAL_TIMEOUTTerminal timed out
1006DECLINEDTransaction declined
1016ABORTEDTransaction aborted
1020INSUFFICIENT_FUNDSInsufficient funds
1099GENERIC_ERRORGeneric error
1100IN_PROGRESSIn progress — continue polling
6000BAD_PARAMSInvalid parameters

TransactionEventId — detailed decline codes

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)
ValueConstantTest amount (cents)
10001REFER_TO_ISSUER
10003INVALID_MERCHANT
10004PICKUP_CARD
10005DO_NOT_HONOR
10006GENERAL_ERROR9906
10012INVALID_TRANSACTION
10013INVALID_AMOUNT
10014INVALID_CARD9914
10030FORMAT_ERROR
10041LOST_CARD
10043STOLEN_CARD9920
10051INSUFFICIENT_FUNDS9951
10054EXPIRED_CARD9954
10055INCORRECT_PIN
10057NOT_PERMITTED_CARDHOLDER9957
10058NOT_PERMITTED_TERMINAL
10061WITHDRAWAL_LIMIT9961
10062RESTRICTED_CARD
10063SECURITY_VIOLATION
10065ACTIVITY_LIMIT
10068LATE_RESPONSE
10070CALL_ISSUER
10075PIN_TRIES_EXCEEDED
10200UNMAPPED

Environment

use QrCommunication\VivaIsv\Enums\Environment;

// Pass as string or enum — both are accepted
$isv = new VivaIsvClient(..., environment: 'demo');
$isv = new VivaIsvClient(..., environment: Environment::PRODUCTION);

IsvConfig::isProduction() / isSandbox() New v1.5.0

Boolean helpers on the config object — convenient for conditional logic without string comparison.

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

// Use in conditional logic
if ($config->isProduction()) {
    logger()->info('Running in production — using real credentials');
}

Error Handling

The SDK defines 3 exceptions in the QrCommunication\VivaIsv\Exceptions namespace:

RuntimeException
└── VivaException         (base — httpStatus, responseBody, getErrorCode(), getErrorText())
    ├── ApiException      (erreurs API 4xx/5xx)
    └── AuthenticationException  (erreurs OAuth2 — httpStatus = 401)
use QrCommunication\VivaIsv\Exceptions\ApiException;
use QrCommunication\VivaIsv\Exceptions\AuthenticationException;

try {
    $order = $isv->orders->create('merchant-uuid', 1500, isvAmount: 100);
} catch (AuthenticationException $e) {
    // Credentials ISV invalides
    echo $e->getMessage(); // 'ISV OAuth2 authentication failed: ...'
} catch (ApiException $e) {
    // Erreur API (400, 404, 500, etc.)
    echo $e->getMessage();       // Message d'erreur
    echo $e->httpStatus;         // Code HTTP
    echo $e->getErrorCode();     // Code Viva (ErrorCode)
    echo $e->getErrorText();     // Texte Viva (ErrorText)
    print_r($e->responseBody);   // Body JSON complet
}
The capture() and recurring() methods throw ApiException if ErrorCode !== 0 in the Viva Wallet response.

Webhook Events (24)

IDType
1796transaction.payment.created
1797transaction.refund.created
1798transaction.payment.cancelled
1799transaction.reversal.created
1800transaction.preauth.created
1801transaction.preauth.completed
1802transaction.preauth.cancelled
1810pos.session.created
1811pos.session.failed
1812transaction.price.calculated
1813transaction.failed
1819account.connected
1820account.verification.status.changed
1821account.transaction.created
1822command.bank.transfer.created
1823command.bank.transfer.executed
1824transfer.created
1825obligation.created
1826obligation.captured
1827order.updated
1828sale.transactions.file

Banking events — merchant-level (v1.5.0) New

These 3 events are registered per connected merchant via /api/messages/config (not via /isv/v1/webhooks). Use the MerchantWebhookRegistrar helper to register them in one call.

IDTypeDescription
768command.bank.transfer.createdBank transfer order created
769command.bank.transfer.executedBank transfer order executed
2054account.transaction.createdAccount (wallet) transaction created
Constant BANKING_EVENTS : The SDK exposes MerchantWebhookRegistrar::BANKING_EVENTS = [768, 769, 2054] to avoid hardcoding these IDs in your application.

Sandbox Testing

Use environment: 'demo' to test without real transactions.

Test card

FieldValue
Number4111 1111 1111 1111
ExpiryAny future date
CVV111

Amounts that trigger declines in demo mode

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);
Amount (cents)Triggered decline
9951Insufficient funds (INSUFFICIENT_FUNDS)
9954Expired card (EXPIRED_CARD)
9920Stolen card (STOLEN_CARD)
9957Card not permitted for cardholder (NOT_PERMITTED_CARDHOLDER)
9961Withdrawal limit exceeded (WITHDRAWAL_LIMIT)
9906General error (GENERAL_ERROR)
9914Invalid card (INVALID_CARD)

Production gotchas (prod-tested 2026-05) v1.6.0

These behaviors were observed on a real production ISV merchant (Merchant 0119432c-...) during a debug session. They are NOT in the official Viva docs.

Endpoints that DON'T work on pure-ISV accounts

EndpointHTTP StatusWorkaround
GET /isv/v1/accounts (isvAccounts->list())405Track accountIds app-side
DELETE /isv/v1/accounts/{id}405No cleanup possible
GET /isv/v1/webhooks (isvWebhooks->list())405Track EventTypeIds app-side
/platforms/v1/accounts/* (Marketplace API)403Use IsvAccounts (/isv/v1/) instead
POST /api/messages/config (isvMessages->register())404Polling reconciliation via wallet API
POST /api/sources (composite auth)400Not needed (Viva default Source)

OAuth split — ISV vs Smart Checkout (mandatory)

ISV credentials only support client_credentials grant. The Authorization Code flow (end-user OAuth login to connect an existing business account) requires separate Smart Checkout credentials created in Viva → Settings → API Access → Smart Checkout. Trying /connect/authorize with ISV credentials always redirects to accounts.vivapayments.com/home/error?errorId=CfDJ8... (errorId encrypted .NET DataProtection — impossible to decode).

Onboarding UX gotchas

  1. Invitation email already linked to a Viva business — The Viva invitation screen shows a "Connect existing" / "Create new business account" selector. Clicking "Connect existing" fails with "Failed to connect with PartnerName" if the Smart Checkout redirect URI is not whitelisted by Viva. Force a clean KYB by using a fresh email (e.g. +tag alias).
  2. accountId may end up linked to a different business email — Case observed: invitation for userA@example.com → selector screen → Continue → OAuth error → the accountId on Viva's side ends up linked to userB@otherdomain.com (multi-business Viva owner). Always verify merchantId and legalName after KYB before considering onboarding successful.
  3. Pings without signature — Viva makes periodic calls (at least hourly) on the webhook URL WITHOUT X-Viva-Signature from Azure IPs (51.138.x.x, 20.54.x.x). Return 200 on these (log warning) — otherwise Viva considers the webhook broken and disables it.

Support tickets to file with Viva

ItemTicket
ISV role activation"Activate ISV Partner Program on merchant {ID}"
Separate Smart Checkout app"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}"
/api/messages/config activation"Enable merchant-level webhook registration on our ISV (events 768/769/2054)" — often refused
/platforms/v1/* activation"Enable Marketplace API on our ISV" — often refused

Full reference in the SDK's skill/references/prod-findings.md .