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
clientId
Settings > API Access > ISV OAuth Credentials > Client ID
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.
// 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).
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.
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).
Method
Signature
Return
verificationResponse
verificationResponse(string $verificationKey)
array{StatusCode, Key}
parse
parse(string $rawBody)
array{event_type, event_type_id, event_data}
isKnownEvent
Webhooks::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->isvMessagesNew 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.
Method
Signature
Return
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);
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.
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.
Value
Constant
Description
0
SUCCESS
Transaction successful
1003
TERMINAL_TIMEOUT
Terminal timed out
1006
DECLINED
Transaction declined
1016
ABORTED
Transaction aborted
1020
INSUFFICIENT_FUNDS
Insufficient funds
1099
GENERIC_ERROR
Generic error
1100
IN_PROGRESS
In progress — continue polling
6000
BAD_PARAMS
Invalid 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)
Value
Constant
Test amount (cents)
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;
// 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:
The capture() and recurring() methods throw ApiException if ErrorCode !== 0 in the Viva Wallet response.
Webhook Events (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
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.
ID
Type
Description
768
command.bank.transfer.created
Bank transfer order created
769
command.bank.transfer.executed
Bank transfer order executed
2054
account.transaction.created
Account (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
Field
Value
Number
4111 1111 1111 1111
Expiry
Any future date
CVV
111
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
9951
Insufficient funds (INSUFFICIENT_FUNDS)
9954
Expired card (EXPIRED_CARD)
9920
Stolen card (STOLEN_CARD)
9957
Card not permitted for cardholder (NOT_PERMITTED_CARDHOLDER)
9961
Withdrawal limit exceeded (WITHDRAWAL_LIMIT)
9906
General error (GENERAL_ERROR)
9914
Invalid 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
Endpoint
HTTP Status
Workaround
GET /isv/v1/accounts (isvAccounts->list())
405
Track accountIds app-side
DELETE /isv/v1/accounts/{id}
405
No cleanup possible
GET /isv/v1/webhooks (isvWebhooks->list())
405
Track EventTypeIds app-side
/platforms/v1/accounts/* (Marketplace API)
403
Use IsvAccounts (/isv/v1/) instead
POST /api/messages/config (isvMessages->register())
404
Polling reconciliation via wallet API
POST /api/sources (composite auth)
400
Not 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
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).
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.
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
Item
Ticket
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