Releases: Setono/quickpay-php-sdk
Release list
v1.2.0
Two follow-ups from putting 1.1 to work in setono/payum-quickpay — Quickpay knowledge every integration re-derives, now in the SDK (#22, #25). Additive; both verified against the live API.
composer require setono/quickpay-php-sdk:^1.2Highlights
- Your callbacks for captures, refunds and cancels now actually arrive. Quickpay POSTs the callback of an API-issued operation to the account-wide callback URL (empty by default) — not to the
callback_urlon the payment link — so a shop that only set the link's URL never heard about them. Every operation now takescallbackUrl:, sent as theQuickPay-Callback-Urlheader (Client::CALLBACK_URL_HEADER) so that operation notifies the URL you name:The low-level$client->payments()->capture($id, new CaptureRequest(1000), callbackUrl: $notifyUrl);
get()/post()/put()/patch()/delete()accept extra request headers too (renew, session, … can send it), andLink::$autoCapture/$autoCaptureAtare typed. (#26) - Read a decline. A declined synchronized capture/refund/cancel is a
2xxwith the decline on the operation.Operation::hasOutcome()/isDeclined()say it plainly, andPaymentgained the views that decide an order's status:latestOperationOfType(),latestApprovedOperation()(where the money actually is — a trailing rejected or pending attempt no longer masks it),hasApprovedOperation(?type), andhasPendingOperation(?type). "Latest" is always the highest operation id, defined once. (#27) order_idfails fast, with the real rule. Live probing showed the API accepts 4–20 characters of letters, digits, space,.,_,-and rejects everything else (/ # : @ % +…, any non-ASCII) under the misleading "must have length between 4 and 20".CreatePaymentRequestnow validates that at construction (ORDER_ID_PATTERN) and throws the newSetono\Quickpay\Exception\InvalidArgumentException— an SPLInvalidArgumentExceptionthat is also aQuickpayException. (#27)paymentMethodsas a list.new CreateLinkRequest(..., paymentMethods: ['creditcard', '!amex'])is joined the way Quickpay expects (a(string)cast of a list used to send the literalArrayand reject every payment). (#27)- README — where operation callbacks go;
statereadspendingduring any asynchronous operation (with the pre-operationbalance); Quickpay does not retry a declined operation (a declined auto-capture leaves the paymentnew/authorized); synchronized declines are2xx; theorderIdrule; the new helpers with a decline-check snippet. The e2eoperate.phpnow routes operation callbacks to the listener.
Changed
CreatePaymentRequestvalidatesorderIdat construction (see above). Code that relied on the API'sValidationExceptionfor a badorder_idnow fails earlier — locally, and with a message naming the actual rule.CollectionRequestOptionsthrowsSetono\Quickpay\Exception\InvalidArgumentException— a subclass of the SPL exception it threw before, so existing catches keep working.
BC notes
ClientInterface::get()/post()/put()/patch()/delete()and the protectedResourceEndpoint::postOperation()gained an optional$headersparameter. Callers are unaffected; only code that implements the mock-only interface (Clientis its sole implementation) or subclasses the SDK's endpoint base (unsupported) needs to add the parameter. The Roave BC check reported these on #26 and it was knowingly merged red.
Quality
210 tests (1.1.0: 166), PHPStan level max, CI across PHP 8.1–8.5 × lowest/highest, plus the Roave backwards-compatibility check on every PR.
Full changelog: v1.1.0...v1.2.0 · CHANGELOG.md
v1.1.0
The developer-experience release. 🧰
v1.0.0 got a top-to-bottom review through a first-time consumer's eyes (#10); this release ships the follow-ups. Everything is additive — no changes to existing signatures for callers — and every API-facing claim was verified against the live Quickpay API.
composer require setono/quickpay-php-sdk:^1.1Highlights
- Find the payment for an order.
$client->payments()->findByOrderId($orderId)— exact match,nullwhen absent. Together withcreate()it makes checkout find-or-create a one-liner (Quickpay enforcesorder_iduniqueness per account, so a retried checkout used to explode with "already exists on another payment"). Listing gained a typedPaymentsQuery(state,accepted,minTime/maxTime,acquirer,sortBy/sortDir, …) forgetPage()/paginate(). (#12) - Read what happened without hand-rolling it.
Operation::isApproved(), and onPayment:authorizedAmount(),capturedAmount(),refundedAmount(),isCancelled(),hasPendingOperation(),latestOperation(),operation($id),operationsOfType(). Only approved operations count — the exact place integrations used to get subtly wrong. (#13) - The whole API within reach.
Client::delete(), plain-array bodies onpost()/put()/patch()(sent as given, so unmodeled endpoints — renew, subscriptions, … — are a one-liner),PaymentsEndpoint::deleteLink(), and a README section on calling endpoints the SDK doesn't model. Bodies arePayload|array $body = [], never null; an empty body goes out as{}. (#14) catch (QuickpayException $e)now really nets everything. Transport failures (DNS, refused connection, timeout) are wrapped inTransportException, which is also a PSR-18ClientExceptionInterface, so existing catches keep working; the original isgetPrevious(),isNetworkError()tells network from request errors. (#18)- Callbacks from any stack.
handleGlobals()for plain PHP (php://input+$_SERVER),handleRaw()now takes the optionalaccountId/apiVersionheaders, and the README has Symfony / Laravel / plain-PHP snippets. (#17) - Production cache in one argument.
new Client('…', cache: new FileSystemCache($dir))(and the same onCallbackHandler) — no more hand-wiring Valinor builders and forgetting the SDK's configuration. (#16) - Small things that add up.
Payment::variables()(your keys, verbatim),Payment::$deadlineAt/$acquirer,CreatePaymentRequest::$shopsystemso a plugin can identify itself, a memoizedCallback::payment(), and the SDK version in theUser-Agent. (#19) - Docs that teach the model, not just the SDK. Table of contents; a "Concepts" section (the two keys, minor units, a payment as a ledger of operations, async operations, the redirect is not proof of payment); "Handling callbacks robustly" (retries → idempotency on operation ids, per-payment ordering, respond fast, why 403/400); an end-to-end checkout recipe and Symfony wiring; accurate install text; a
CHANGELOG.md; docs shipped in the dist again. (#20, #21) - Safety and hygiene.
Client::request()now runs the same host-pinning guard as every other method (a consumer-built request could previously carry the API key to another host — #11). PRs are now gated by a Roave BC check (#23).
BC notes
Nothing changes for code that calls the SDK. Two things could touch unusual code:
ClientInterfacegaineddelete(), andpost()/put()/patch()takePayload|array $body = []instead of?Payload $body = null— relevant only if you implement the interface (a mock-only interface) or passed an explicitnullbody (pass nothing, or[]). (#14)- Code that caught
Psr\Http\Client\NetworkExceptionInterface/RequestExceptionInterfacespecifically (rather thanClientExceptionInterface) should catchTransportExceptionand inspectgetPrevious()/isNetworkError(). (#18) CollectionRequestOptionsis no longerfinal, and itspage/pageSizeare plain public properties (a readonly property cannot be reinitialized duringclonebefore PHP 8.3) — the withers still validate. (#12)
Won't do
- A shipped test double (#15): the seam is the PSR-18 client, and every stack already has a mock for it — the README now shows how to plug it in.
Quality
166 tests (was 109), PHPStan level max, MSI gate unchanged, CI across PHP 8.1–8.5 × lowest/highest, plus the new backwards-compatibility check on every PR — and a live smoke run (create → link → get) against the real API on the merged branch.
Full changelog: v1.0.0...v1.1.0 · CHANGELOG.md
v1.0.0
First stable release. 🎉
A small, strongly-typed PHP SDK for the Quickpay API (v10), deliberately focused on the payments resource, the /ping health check, the payment-window link flow, and callback (webhook) verification.
composer require setono/quickpay-php-sdkRequires PHP >= 8.1 and any PSR-18 HTTP client / PSR-17 factories (auto-discovered via php-http/discovery).
Highlights
- Typed end to end — request DTOs with live-API-verified required fields, response DTOs mapped with Valinor, and a full typed exception hierarchy under a single
QuickpayExceptionmarker interface. - Safe by default — host pinning so credentials only ever go to
api.quickpay.net, sanitized URLs in exception messages, timing-safe HMAC verification of callbacks over the raw request body, and strict validation of theQuickPay-Resource-Typeheader. - Resilient to API evolution — non-exhaustive
PaymentState/OperationTypeenums that never throw on server-added values, and a$rawescape hatch on every response for fields the SDK doesn't model. - Async operations, modeled honestly — capture/refund/cancel/authorize support Quickpay's
?synchronizedflag per call or as a client-wide default, and the docs explain exactly what a202 Acceptedresponse does (and does not) tell you. - Well-tested — 109 unit tests, PHPStan at level max, mutation score (MSI) 85%, CI across PHP 8.1–8.5 with lowest & highest dependencies, plus a committed end-to-end harness (
examples/e2e/) for verifying the whole flow against the real API.
See the README for full usage — from creating a payment and redirecting to the payment window, to verifying the signed callback.
Changes since v1.0.0-beta.2
- Dist installs are now lean:
CLAUDE.md,examples/and.env.local.exampleareexport-ignored, so acomposer requireships onlyLICENSE,composer.jsonandsrc/(#9) - Fixed the live test's generated
order_idexceeding Quickpay's 20-character limit (#9) - Fixed a docblock referencing the nonexistent
CallbackHandler::handleRequest()(nowhandleRaw()) (#9) - Removed an undefined
env.PHP_EXTENSIONSreference from the CI workflow (#9)
Full changelog: v1.0.0-beta.2...v1.0.0
v1.0.0-beta.2
Dependency slim-down, plus one small deliberate BC cleanup (#8) — the last of its kind planned before v1.0.0.
Changed
webmozart/assertis no longer a dependency.CollectionRequestOptionswas its only user in the SDK (two>= 1checks); they are now plain inline guards throwing\InvalidArgumentException. Since Webmozart's exception already extended\InvalidArgumentException, catch sites see no behavioral difference — but every consumer's dependency tree gets one package lighter.
Removed (BC break)
-
CollectionRequestOptions::new(). It existed only to start fluent wither chains (::new()->withPageSize(50)); constructor named arguments express that better:new CollectionRequestOptions(pageSize: 50). The withers (withPage()/withPageSize()) remain. If you call::new(), replace it withnew CollectionRequestOptions().beta.1 stated no further BC breaks were planned — this cosmetic removal was judged worth the exception as a last-cheap-moment cleanup while still pre-1.0. Apologies if it bites; the fix is mechanical.
Full changelog: v1.0.0-beta.1...v1.0.0-beta.2
v1.0.0-beta.1
First beta — the API surface is now considered stable for 1.0. The alpha series deliberately made its breaking changes early; no further BC breaks are planned before v1.0.0, and remaining work is feedback, polish and documentation.
Where the SDK stands
- Scope: the
paymentsresource (create/read/list/update + authorize/capture/refund/cancel), the/pinghealth check, the payment-window link flow, and callback (webhook) verification — deliberately narrow and typed. - Contracts verified against the live API, not the docs: required request fields are required constructor arguments (
orderId/currencyon create,amounton links and operations, all fiveBasketItemfields);Address/Shippingverified lenient; response quirks (e.g.is3d_securereturning a boolean) modeled from real responses. - Async operations documented honestly: without
?synchronizedthe API answers 202 with a queued-operation snapshot — the docblocks explain what you can and cannot read from it, and the client-widesynchronizedconstructor flag sets the default once. - Callback verification is deliberately un-mockable:
CallbackHandler/CallbackValidatorarefinalwith no interfaces; the README's new "Testing your callback endpoint" section (#7, the one change since alpha.4) documents the intended pattern — a real handler with a test key andvalidator()->sign()-forged signatures. - Quality gates: 109 tests across PHP 8.1–8.5 × lowest/highest, PHPStan level max, mutation score MSI 85% / covered 85% (gate 70), plus a committed real-API e2e harness (
examples/e2e/).
Changed since v1.0.0-alpha.4
- README: "Testing your callback endpoint" section (#7). Docs only — no code changes.
Full changelog: v1.0.0-alpha.4...v1.0.0-beta.1
v1.0.0-alpha.4
Internal-quality release — no API changes and no BC impact.
Improved
ResponseAwareExceptionis now fully covered (#5): the lazy stream-read parse path,getResponse(), the integererror_codecast and the non-maperrorsbranch all gained tests, plus two behavior pins — the pre-read body winning over an already-drained (non-seekable) response stream, and the default message's query/fragment stripping asserted character-exact so the secret-leak guard cannot silently regress. Suite mutation score: MSI 80% → 85%, covered MSI 82% → 85%.- Consistent
ResourceEndpointhelper naming (#6):update()→updateOne(),operation()→postOperation(),putSub()→putSubResource()— every helper now follows either semanticVerb + One (typed single-resource CRUD) or httpVerb + target (transport). Internal-only: the helpers areprotectedand endpoint subclassing is outside the BC promise. Also fixes the class docblock'supdate()bullet, which said PUT while the helper sends PATCH.
Full changelog: v1.0.0-alpha.3...v1.0.0-alpha.4
v1.0.0-alpha.3
Breaking changes
Unconditionally-required request fields are now required constructor parameters (#2, #3). Every field was verified against the live Quickpay API (not just the docs) before being tightened; all of them were already the first constructor parameters, so positional callers are unaffected:
CreatePaymentRequest::$orderId,::$currency$amountonCreateLinkRequest,CaptureRequest,RefundRequest,AuthorizePaymentRequest— note the API does not fall back to capturing/refunding the remaining balance whenamountis omitted; it rejects the operation- all five
BasketItemfields (qty,itemNo,itemName,itemPrice,vatRate) — the API treats a basket item as all-or-nothing, and a fully-empty item even triggered an HTTP 500 on Quickpay's side PaymentsEndpoint::authorize()now requires its$request(a bodyless authorize can never succeed)
A missing required field now fails at the call site (and in static analysis) instead of surfacing as a ValidationException after a network round-trip. Address and Shipping stay all-optional — verified lenient against the live API. Beware Shipping::$method: when present it must be one of Quickpay's accepted values.
Fixed
- A
Payloadwith no set fields was serialized as[](a JSON array), which the API always rejects (body: "is invalid") — e.g.updatePayment($id, new UpdatePaymentRequest())could never succeed. It is now sent as{}. (#2) updatePayment()'s docblock said PUT; the SDK sends PATCH. (#4)
Improved
- Docblocks on
authorize/capture/refund/cancelnow document what the async (202) response actually contains — a snapshot taken when the operation was queued (pending: true, pre-operationstate/balance) — and how to confirm the outcome (callback, or pollgetById()). - Test suite audit: 92 → 103 tests; mutation score MSI 72% → 80%, covered MSI 76% → 82%. New regression guards include the strict-mapping failure path (
MappingException), both supported date formats, 202 handling, and the host-pinning guard's edge cases. (#4) - README: documents the required-constructor-argument rule and fixes the
ValidationExceptionexample's order id (3 chars would nowadays fail the API's 4–20 length rule). (#4)
Full changelog: v1.0.0-alpha.2...v1.0.0-alpha.3
v1.0.0-alpha.2
What's new
Client-wide default for the synchronized operation flag (#1)
The payment operation methods (authorize / capture / refund / cancel) now take ?bool $synchronized = null. When null (the default) they fall back to a new synchronized flag on the Client constructor, so integrations that always want to wait for the completed transaction set it once instead of repeating it on every call — a non-null per-call argument still overrides the default:
$client = new Client('YOUR_API_KEY', synchronized: true);
$client->payments()->capture($id, new CaptureRequest(1000)); // waits (client default)
$client->payments()->refund($id, new RefundRequest(250), synchronized: false); // per-call overrideThe default is exposed as ClientInterface::isSynchronized(). Behavior is unchanged for existing code.
BC note
ClientInterface gained the isSynchronized() method — custom implementations or decorators of that interface must add it. Code using Client directly is unaffected.
Full changelog: v1.0.0-alpha.1...v1.0.0-alpha.2
v1.0.0-alpha.1
First alpha of the Quickpay PHP SDK — a small, strongly-typed client for the Quickpay API.
⚠️ Alpha — the public API may still change before1.0.0.
Features
- Payments: create, get, list/paginate, update (
PATCH), authorize, capture, refund, cancel — with an optionalsynchronizedflag to wait for the completed transaction. - Payment-window link flow:
createLink()returns the hosted payment-window URL to redirect the customer to. - Callbacks (webhooks): timing-safe checksum verification (
hash_equalsover the raw body, account private key) + typed deserialization, with strictQuickPay-Resource-Typehandling (Payment/Subscription). /pinghealth check.- Typed request/response DTOs with a
$rawfallback for unmodeled fields, and a typed exception hierarchy (everything implementsQuickpayException). - Credential-leak protection: the client refuses to send to any host other than
api.quickpay.net.
Requirements
- PHP 8.1 – 8.5
- A PSR-18 HTTP client + PSR-17 factories (auto-discovered via
php-http/discovery)
Install
composer require setono/quickpay-php-sdk:"^1.0@alpha"
# plus an HTTP client + PSR-17 factory if you don't have one, e.g.:
composer require kriswallsmith/buzz nyholm/psr7See the README for usage, the callback flow, and the end-to-end test harness.
Quality
Tested against PHP 8.1–8.5 (lowest + highest deps), PHPStan level max, ECS, Rector, and mutation testing (Stryker), plus a real-API end-to-end verification of the payment + callback flow.