transbank/transbank-sdk
Official PHP SDK for integrating Transbank payment services, including Webpay Plus, Oneclick Mall, Patpass, and full transaction flows. Provides REST/SOAP clients, configuration helpers, and ready-to-use classes for Laravel and PHP apps, simplifying payment initiation, confirmation, refunds, and sta...
composer require transbank/transbank-sdk
The SDK is a thin REST client — no service provider, no config file, no facades. Everything is driven by an Options object that carries the environment + credentials, and every resource (WebpayPlus\Transaction, WebpayPlus\MallTransaction, Oneclick\MallInscription, Oneclick\MallTransaction, Oneclick\MallBinInfo, TransaccionCompleta\Transaction, Patpass\PatpassComercio\Inscription) is instantiated with ::build($options).
Where to look first (in order):
README.md — it contains a working Webpay Plus create → commit snippet; copy it verbatim as a smoke test.src/Webpay/Options.php — forIntegration($commerceCode, $apiKey) / forProduction(...) and fromArray() if you want to load from config.src/Webpay/WebpayPlus/Transaction.php — the reference implementation; every other product mirrors its shape (create, commit, refund, capture, status).src/Webpay/WebpayPlus/Responses/ — the response DTOs; the getters here are your actual integration surface.First use case — Webpay Plus, the 90% case. Two endpoints: one that creates the transaction and redirects, one that receives Transbank's POST-back and commits.
use Transbank\Webpay\Options;
use Transbank\Webpay\WebpayPlus\Transaction;
$options = Options::forIntegration('597055555532', '579B532A...A36B1C');
$tx = Transaction::build($options);
// POST /checkout
$response = $tx->create(
buyOrder: 'ORDER-'.$order->id,
sessionId: (string) $order->id,
amount: $order->total, // CLP integer, no decimals
returnUrl: route('webpay.return') // must be publicly reachable
);
return redirect($response->getUrl().'?token_ws='.$response->getToken());
// POST /webpay/return (Transbank POSTs here)
$result = $tx->commit(request('token_ws'));
if ($result->getResponseCode() === 0 && $result->getStatus() === 'AUTHORIZED') {
$order->markPaid($result->getAuthorizationCode());
}
Integration credentials for Webpay Plus are the well-known public test pair (597055555532 / 579B532A7440BB0C9079DED94D31EA1615BACEB56610332264630D42D0A36B1C); Mall, Oneclick and Patpass each have their own test commerce codes documented in the README. Never ship those into a production config default.
Wrap it once, inject everywhere. The static ::build() is convenient but hostile to testing. Bind a singleton in a service provider and inject it:
$this->app->singleton(Transaction::class, fn () => Transaction::build(
Options::fromArray([
'commerce_code' => config('transbank.commerce_code'),
'api_key' => config('transbank.api_key'),
'environment' => config('transbank.environment'), // integration|production
])
));
Two-route controller, nothing more. The checkout route persists a Payment row before calling create() and stores the returned token against the order. The return route looks the order up by token_ws (or buy_order), never by anything in the request body. This is the single design decision that makes the rest of the integration safe.
Status is a state machine, not a boolean. Persist (status, response_code, authorization_code, amount, card_last4, payment_type_code, installments) on every commit, even failures — reconciliation and support tickets live or die on those fields.
Mall / multi-seller. Same flow, but create() takes a $details array (one entry per child commerce code with its own commerce_code, buy_order, amount, installments_number) and commit() returns a detail collection you must iterate — each child has its own response_code. Don't assume the parent-level status implies success for every child.
Oneclick for stored cards. MallInscription::build($options)->start($username, $email, $responseUrl) → user is redirected → finish($token) yields getTbkUser() and getUsername(). Store both; they're the long-lived handle. Subsequent charges are MallTransaction::build($options)->authorize($username, $tbkUser, $parentBuyOrder, $details). Treat tbkUser as a secret (it's a bearer-like card reference) and expect it to be invalidated when the card is reissued — handle the response_code for "inscription not found" by falling back to re-inscription.
Inspecting a stored card (new in 5.1.0). Oneclick\MallBinInfo::build($options)->queryBin($tbkUser) returns BIN-level information for a card already inscribed under your commerce code — useful for showing the card brand/type next to a saved card, or for deciding whether to offer installments, without charging anything. It's the only addition in 5.1.0; every pre-existing method is untouched, so it is a safe minor bump. Cache the result against the inscription row (BIN data changes only when the card is reissued) rather than calling it on every page render, and treat a failed lookup as "unknown card", not as an error that blocks checkout — the charge path (MallTransaction::authorize) is what actually matters.
Deferred capture & refunds. WebpayPlus\Transaction::capture($token, $buyOrder, $authorizationCode, $captureAmount) for deferred flows, refund($token, $amount) for post-settlement. Both need the original token/authorization code — another reason to persist the full commit payload. Refunds on Mall are per child commerce code.
Reconciliation job. A scheduled command that queries Transaction::status($token) for anything stuck in PENDING is worth the 30 lines; Transbank's POST-back will occasionally never arrive (user closed the tab on the redirect page, network blip), and the money will still have moved.
Local development. There is no official fake or sandbox simulator object. Either point at integration (real HTTP, real latency) behind a feature flag, or wrap the SDK behind your own PaymentGateway interface and bind a fake in tests. Trying to mock Guzzle underneath the SDK is more painful than mocking your own interface.
Route::post(...) and excluded from CSRF (->withoutMiddleware(VerifyCsrfToken::class) or the api middleware group). This is the #1 first-day blocker.token_ws. Abort/timeout: TBK_TOKEN + TBK_ORDEN_COMPRA + TBK_ID_SESION and no token_ws — you cannot call commit() in that case. Branch on request()->has('token_ws') before anything else.commit() re-fetches the authoritative amount from Transbank. Compare $result->getAmount() against your stored order total and flag mismatches instead of silently accepting.commit() is not idempotent and the browser may double-submit. Guard with a DB transaction + row lock on the order, and short-circuit if the order is already paid. Double fulfillment is the expensive bug in this integration.commit() on an expired token returns a non-zero response_code rather than throwing. Distinguish "expired" from "rejected" in your UX.response_code === 0 is the approval signal, status === 'AUTHORIZED' is corroboration. NULLIFIED and REVERSED are valid states that appear later, so don't treat a non-authorized status as terminal in your own model..00. Passing 1990.0 works; passing 19.90 for a 19-peso charge silently charges 19 pesos.TransbankException / Guzzle errors mean the call didn't complete — you don't know the outcome. Log the token and reconcile via status() rather than retrying blindly. Configure Guzzle timeouts explicitly if you wrap the client; default timeouts are long enough to hold a PHP-FPM worker hostage.create() fails at runtime with an unhelpful message. Test with the documented test codes before assuming a code bug.MallBinInfo::queryBin() output identifies a cardholder's card; keep it out of logs, out of client-side JS payloads beyond what you render, and inside the same retention policy as card_last4. It is also not a validation oracle: a 200 response does not prove the card is still chargeable, and a failed lookup does not mean the inscription is dead. Only authorize() tells you that.configureForIntegration statics → Options + ::build(), Transbank\Webpay namespace restructuring). Lock the major in composer.json, read the changelog before bumping, and expect to be your own support — but note that it is maintained by Transbank directly, with releases through 2025, so the risk is API churn rather than abandonment. 5.1.0 is purely additive (MallBinInfo), so ^5.1 is a low-risk constraint if you're already on 5.x.How can I help you explore Laravel packages today?