univapay / univapay-sdk-compat
Runtime compatibility layer that reimplements the public surface of univapay/php-sdk (the legacy hand-written SDK) on top of the APIMatic-generated univapay-client-php-sdk transport engine, so existing integrators keep working after migrating with univapay/univapay-sdk-migrate.
Requires
- php: >=7.2
- ext-json: *
- moneyphp/money: ^3.3 || ^4.0
- univapay/client-sdk: ^1.2.0
Requires (Dev)
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- phpcompatibility/php-compatibility: ^9.3
- phpunit/phpunit: ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.5 || ^13.0
- squizlabs/php_codesniffer: ^3.7
This package is auto-updated.
Last update: 2026-08-19 04:20:12 UTC
README
A runtime compatibility layer that reimplements the public surface of the legacy, hand-written
univapay/php-sdk — the same class names, method
signatures, public properties, enum style, exceptions, and polling behavior — on top of
univapay/client-sdk (the new,
APIMatic-generated transport engine) as its transport. It exists so that a codebase migrated by
univapay/univapay-sdk-migrate keeps
working, unmodified, on the new engine.
What this is
univapay/php-sdk will not be updated again. Every new API feature (v1.1 and later) lands only in
univapay/client-sdk. This package lets existing integrators reach that new engine without
rewriting call sites: it is a drop-in, namespace-only swap — Univapay\* → Univapay\Compat\* —
that keeps every construct your code already uses (Money\Money values, ChargeStatus::SUCCESSFUL()
identity comparisons, public property access, awaitResult()/chained calls, catch blocks) behaving
exactly as it did before. Internally, every compat method builds a typed request against
univapay/client-sdk and sends it through that engine's real HTTP transport — nothing here talks to
the API directly. See Architecture for how responses are then hydrated back into
the old SDK's shapes.
You do not install this package by hand in the normal case — see Install.
Install
Normal path: run the migration tool. univapay/univapay-sdk-migrate requires this package,
rewrites your code's imports to point at it, and removes univapay/php-sdk, all in one command:
composer require --dev univapay/univapay-sdk-migrate vendor/bin/univapay-migrate
See the univapay-sdk-migrate README for
what that command does, its flags, and its report format.
Manual path. If you are not using the migration tool — a fresh integration deliberately targeting the old SDK's API shape, or a codebase already rewritten by hand — install directly:
composer require univapay/univapay-sdk-compat
The two packages can be installed side by side — their autoload roots (Univapay\ vs.
Univapay\Compat\) don't collide, and this package never references an old-SDK class directly.
univapay-sdk-migrate relies on that: it requires this package before removing univapay/php-sdk,
so both are present for one step while Rector still needs the old SDK's classes loadable for
receiver-type resolution. Once your migration is done, remove univapay/php-sdk — there's no
reason to keep it installed, but nothing breaks if it lingers alongside this package. Requires PHP
>=7.2 (matching univapay/client-sdk's own floor) and moneyphp/money ^3.3 || ^4.0.
Supported surface matrix
Almost everything the old SDK exposed works. A small, fixed set of methods compile and are
reachable but throw Errors\UnivapayUnsupportedFeatureError at call time, because the new engine
has no equivalent API to call through to:
| Area | Status | Notes |
|---|---|---|
| Charges, Refunds, Cancels | Live | Full lifecycle, including the two-step token-GET-then-create preflight the old SDK performed. |
| Subscriptions, Scheduled Payments | Live | |
| Transaction Tokens (card, Konbini, online wallets, Paidy, bank transfer, QR) | Live | |
| Stores, Merchants, Configuration | Live | Except update() — see below. |
| Transaction History | Live | Read-only: GET /transaction_history. |
Webhook parsing (parseWebhookData) |
Live | See Webhook notes for corner cases carried over verbatim. |
Store::update(), Merchant::update() |
Permanent throw | No update endpoint for either resource was ever exposed by the old SDK or the backend — this isn't an engine gap, there is nothing to call. |
Transfer, TransferStatusChange, Ledger (and the GetTransfers/GetLedgers/GetStatusChanges mixins) |
Permanent throw | The new engine has no Transfers API at all — no controller, no listing, no fetch. Transfer webhook events still hydrate (see Webhook notes); every subsequent call on that object throws. |
BankAccount (and the GetBankAccounts mixin) |
Permanent throw | The new engine has no Bank Accounts API at all — no controller, no listing, no fetch, no update. Unlike Transfer, the old SDK's webhook events never carried a bank account payload either, so there is no live channel this class still serves; it remains a hydration-capable data class purely for parity with every other ported resource. |
ApplePayPayment token creation |
Permanent throw | Constructing the value object still works; creating a token from it does not — Apple Pay isn't wired into the new engine. |
Charge::qrMerchantToken() |
Permanent throw | Only this one method — Charge itself is fully supported. The underlying /qr endpoint is deprecated upstream; MPM QR data is available from the token object instead. |
"Permanent throw" means feature-frozen: these will not gain support in a future compat release. They are reachable only through the native SDK (see Migrating off the compat layer), and the migration tool flags every call site that reaches one so it's a reviewable line instead of a runtime surprise.
Behavior deltas
Compat is not a byte-for-byte replay of the old SDK — a small number of documented, deliberate differences exist.
| Area | Behavior |
|---|---|
listTransactions() |
Null-safe on $from/$to — omitting either (e.g. to filter by status alone) no longer fatals. |
| Card token hydration | billing/three_ds are nullable — absent on the wire yields null, not a TypeError. |
| CVV authorization status | CvvAuthorizationStatus::ERROR() exists for the backend's error status value. |
| Plan types | InstallmentPlanType::FIXED_CYCLE_AMOUNT() and SubscriptionPlanType::REVOLVING() exist for backend plan_type values the old lookups lacked (each fataled with OutOfRangeException). |
CheckoutInfo |
supportedCurrencies is nullable — null when the server omits it, not a fatal. |
| Bank-transfer issuer token | call_method is optional — null when the payload omits it (other payment types are unaffected). |
| Paidy token | phone_number hydrates as a plain string, not the nested {country_code, local_number} shape. |
Beyond those, several other differences are deliberate:
UnivapayNetworkErrorreplaces the oldWpOrg\Requests\Exceptionretry target. A genuine transport failure (DNS, connection refused, timeout before any response) surfaces from the new engine asApiExceptionwithgetCode() === 0. The old SDK'sNetworkRetryHandlermatched onWpOrg\Requests\Exception, a class that never appears on this transport — so that retry path was silently dead. Compat'sNetworkRetryHandlertargetsErrors\UnivapayNetworkErrorinstead, whichSupport\ExceptionMapperraises specifically for this case (notUnivapayServerError, which would mislabel a network failure as a 5xx). The migration tool flags any consumer code that still catchesWpOrg\Requests\Exceptionfor manual review.- 10-second timeout, matching what integrators have always experienced. The old SDK's transport
(
rmccue/requests) defaulted to 10s and never exposed the knob. The new engine's own default is 30s;Support\Bridgepins it back to 10 so nothing that depended on that ceiling changes behavior. - Retry-safe idempotency. The old SDK generated one idempotency key per logical call and reused
it on every retry within that call. The new engine's
IdempotencyCallbackmints a fresh key per HTTP request by default — combined with the default retry cascade (rate-limit + network retries, up to 4 attempts), a timed-out-but-actually-processedPOST /chargescould create up to 4 real charges before this fix.Support\ApiCallergenerates one key per logical call, outside the retry loop, and passes it explicitly on every attempt — exactly what the old SDK did. - Error mapping goes through
ApiResponse::isError(), not a caught exception. Every generated API method's response handler is configured to return an error response rather than throw one — a 4xx/5xx fromunivapay/client-sdkcomes back as a plain, non-throwingApiResponsewhoseisError()istrue, not an exception.Support\ApiCallerchecks for that on every call and maps it viaSupport\ExceptionMapperinto the sameErrors\*hierarchy the old SDK exposed (UnivapayNotFoundError,UnivapayRequestError, etc.) — including the old SDK's own quirk that 404 responses carry no decoded error body (only 400/401/403 do). A genuine transport failure (no HTTP response at all) is the one case that does still throw, and is mapped separately intoUnivapayNetworkErrorabove. Seedocs/ARCHITECTURE.mdfor the full mechanism and why it matters.
Webhook notes
UnivapayClient::parseWebhookData() reproduces the old SDK's dispatch and its corner cases
verbatim — including ones that look like bugs but are pinned, intentional behavior:
- Transfer events hydrate; everything else about
Transferstill throws.transfer_created/transfer_updated/transfer_finalizedwebhook payloads hydrate a realResources\Transferobject regardless of the fact thatTransferitself is unsupported for direct API access (see the surface matrix above) — the webhook channel keeps delivering that data independent of whether this transport engine exposes a Transfers API. Any subsequent call on that object —fetch(),update(),listLedgers(),listStatusChanges()— throwsUnivapayUnsupportedFeatureError. Parsing the payload does not make the resource supported. - Three current token event types have no compat enum case. The live API's
TokenEventdiscriminator now includestoken_three_d_s_updated,token_cvv_auth_check_updated, andtoken_replaced— additions made after the old SDK'sEnums\WebhookEventwas last updated, so none of the three exist in compat's ported version of that enum either (it mirrors the old SDK, not the current spec). A webhook delivery carrying one of these three event types will raiseErrors\UnivapayUnknownWebhookEvent, exactly as any other unrecognizedeventstring would. If your integration needs these events, handle them vianative()(see below) instead ofparseWebhookData(). - A merchant-level app token receiving a store-scoped event gets
UnivapayInvalidWebhookData, not a clearer error.TOKEN_*,REFUND_FINISHED, andCANCEL_FINISHEDevents require a store-scoped JWT, exactly as the old SDK's context lookups did; the guard that enforces this fires inside the sametryblock that a broadcatchfunnels intoUnivapayInvalidWebhookData— so that's what a merchant-JWT client sees, not a more specific "wrong token type" error. This is reproduced exactly, not cleaned up, because the old SDK's own behavior here is what any existing integration has already coded around. customs_declaration_finishedhas an enum case but no parser — it also maps toUnivapayInvalidWebhookData. The event type is recognized (it doesn't raiseUnivapayUnknownWebhookEvent), but there has never been a resource type for it to hydrate into, in the old SDK or here.
Architecture
Requests are built typed, against univapay/client-sdk's own generated models — every field your
code sets goes through the same validation and serialization the native SDK would use. Responses,
by contrast, are hydrated from the raw captured wire body through the old SDK's own ported JSON
schema parsers, not through the generated SDK's typed response models — the only way to guarantee
wire-for-wire parity with what the old SDK's battle-tested parsers already handled (including shapes
the current spec doesn't describe yet). See docs/ARCHITECTURE.md for the
full request/response diagram and the confinement boundary that keeps raw-body access contained to
a reviewed allowlist of files.
Migrating off the compat layer
Compat is not meant to live forever. UnivapayClient::native() returns the exact
UnivaPay\UnivapayClientSdkClient instance this client already built internally to make its own
calls — same auth, base URL, and 10-second timeout as the compat surface, never a second,
separately configured client:
$client = new UnivapayClient($storeAppToken); // Compat surface -- unchanged. $charge = $client->createToken($paymentMethod)->createCharge(Money::USD(1000))->awaitResult(); // Native surface -- same engine, same auth, same connection settings. $native = $client->native(); $chargesApi = $native->getChargesApi();
This enables mixed mode: migrate call sites file by file, rewriting each one against native()'s
typed API while everything not yet touched keeps calling the compat facade exactly as before. Both
paths share one engine, so there is no drift between them during the migration window — a charge
created through native() is visible to code still reading it through compat, and vice versa.
A full construct-by-construct migration reference — Money → int + currency string,
ChargeStatus::SUCCESSFUL() identity comparisons → string constants, $charge->status → typed
getters, awaitResult() → pollCharge(), paginated lists → cursor parameters, parseWebhookData()
→ typed webhook handler classes, and more, each with a before/after snippet — is tracked in the
portal guide's Phase 2 section,
not duplicated here.
Deprecation notices
Opt-in runtime signal that helps a team find its own remaining compat call sites before doing the
phase-2 migration onto native() above.
$options = new UnivapayClientOptions(); $options->deprecationNotices = true; $client = new UnivapayClient($storeAppToken, $options); $client->createCharge($tokenId, Money::USD(1000)); // -> E_USER_DEPRECATED: Univapay\Compat\UnivapayClient::createCharge() is a compatibility-layer // method; the native equivalent is ChargesApi::createCharge() via native(). See <guide-url>
UnivapayClientOptions::$deprecationNotices—bool, defaults tofalse. Off is zero overhead: no backtrace, no bookkeeping, just one boolean check.- On, every public compat method (
UnivapayClientmethods, resource methods —fetch()/patch()/capture()/cancel()/awaitResult()/createRefund()/etc. — list methods,parseWebhookData()) triggers onetrigger_error(..., E_USER_DEPRECATED)the first time it's reached from a given line of your code; calling that same line again stays silent, a different line notifies again. Each message names the compat method and its native-SDK equivalent. - No cascade. A compat method that calls other compat methods internally —
createCharge()'s own token-fetch-then-create flow, for instance — emits only the one notice for the call your code actually made, not one per internal step. native()never notifies. It's the escape hatch this feature is steering you towards, not a call site to flag.- Turn it on in a staging/dev environment, exercise real traffic, and the notices surface exactly
the call sites
univapay-sdk-migrate's--phase2Rector set will also want you to look at — the two are meant to be used together, not as alternatives to each other.
Versioning and sunset policy
Compat is feature-frozen as of 1.0: every new API capability lands in univapay/client-sdk only,
reachable through native(). Compat itself continues to receive bugfixes and follows the engine
SDK's own version bumps, for as long as integrators still depend on the old SDK's surface — there is
no forced end-of-life date. That asymmetry (old surface stays exactly as it is; new capability only
exists on the other side of native()) is the intended, gradual pressure to migrate, not a cliff.
License
MIT.