omnipay/braintree is a driver that plugs Braintree into the Omnipay abstraction. You still do the hard part (Braintree's Drop-in UI / Hosted Fields) on the frontend to produce a payment_method_nonce; this package is only the server-side call that turns that nonce into a transaction.
composer require omnipay/braintree:^2.0 omnipay/common. The driver pins fairly old major versions of braintree/braintree_php and omnipay/common (last tagged release was 2019), so check composer why-not braintree/braintree_php 6.x early rather than discovering the conflict while wiring the gateway.src/Gateway.php (which requests actually exist) and then the corresponding src/Message/*Request.php (which input keys each request accepts). Omnipay's AbstractRequest gives you amount, currency, description, transactionReference, token and card for free; everything Braintree-specific lives in the driver's request classes.use Omnipay\Omnipay;
$gateway = Omnipay::create('Braintree'); // resolves Omnipay\Braintree\Gateway
$gateway->initialize([
'merchantId' => config('services.braintree.merchant_id'),
'publicKey' => config('services.braintree.public_key'),
'privateKey' => config('services.braintree.private_key'),
'testMode' => app()->environment('production') === false,
]);
token:$response = $gateway->purchase([
'amount' => '12.50',
'currency' => 'USD',
'token' => $request->input('payment_method_nonce'),
])->send();
if ($response->isSuccessful()) {
Order::create([
'transaction_ref' => $response->getTransactionReference(),
'status' => 'paid',
]);
} else {
report(new \RuntimeException($response->getMessage()));
}
In the sandbox, nonces like fake-valid-nonce, fake-valid-visa-nonce and fake-processor-declined-visa-nonce let you exercise success and decline paths without touching the browser.
Authorize / capture. Use authorize() when you need to ship before settling, then capture() against the Braintree transaction ID. A void before capture releases the hold; a refund only applies after settlement.
$auth = $gateway->authorize(['amount' => '40.00', 'currency' => 'USD', 'token' => $nonce])->send();
$ref = $auth->getTransactionReference();
// later, once fulfilled
$gateway->capture(['amount' => '40.00', 'transactionReference' => $ref])->send();
// or: $gateway->void(['transactionReference' => $ref])->send();
Refunds need the original transactionReference; omit amount for a full refund, supply it for partials. Braintree rejects refunds that exceed the remaining refundable amount, so track that yourself rather than relying on the gateway.
Vaulting (repeat customers). Two-step: create a customer, then attach a payment method. Store the returned Braintree customer ID and payment method token in your users/payment_methods table — the driver is stateless.
$customer = $gateway->createCustomer([
'firstName' => 'Ada', 'lastName' => 'Lovelace',
'email' => '[email protected]',
])->send();
$customerRef = $customer->getCustomerReference();
$card = $gateway->createCard([
'customerReference' => $customerRef,
'token' => $nonce, // nonce from the browser
])->send();
$cardToken = $card->getToken();
Subsequent charges then use token => $cardToken (and customerReference where the request requires it) with no browser round-trip.
Subscriptions. createSubscription(), updateSubscription() and cancelSubscription() map to Braintree plans. You'll need a planId configured in the Braintree control panel and a payment method token attached to the customer first — subscriptions are keyed on the stored payment method, not a nonce.
Webhooks are not part of this package. Handle subscription_charged_successfully, subscription_went_past_due, disbursement, etc. separately with braintree/braintree_php's Braintree\Webhook::parse() in a controller that verifies the bt_signature/bt_payload params. Keep that handler isolated from the Omnipay path; the two do not share a response object.
Wrapping it for testability. Since Omnipay::create() returns a concrete gateway, bind it behind an interface or expose a PaymentService that accepts an injected Gateway. Mock PurchaseResponse rather than the SDK so unit tests stay fast and don't need the sandbox.
Multi-gateway apps are where this package earns its keep — the same purchase()->send()->isSuccessful() flow works for Stripe, PayPal, etc. If Braintree is your only processor, consider calling braintree/braintree_php directly; the abstraction costs you type information for little gain.
dev-master pin or a fork if you need PHP 8 compatibility, newer braintree_php, or bug fixes.token is overloaded. It means payment method nonce for one-off purchases and stored payment method token for vaulted charges. Confirm per request class in src/Message — this is the single most common source of "why is my stored card charging a nonce" bugs.card => [...], stop and build the Drop-in/Hosted Fields integration instead.braintree_php for the 3DS challenge step and only hand the final nonce to Omnipay."12.50". Convert with number_format($cents / 100, 2, '.', '') in one place; float rounding bugs show up as declined or off-by-a-cent transactions.isSuccessful() === false covers both declines and transport errors. Inspect getMessage() and getCode() (Braintree's status/reason text) and log both — Braintree's decline reasons like 2000 (do not honor) are far more useful in support tickets than the generic message.Braintree_Gateway internally, the simplest logging hook is \Braintree\Configuration::setLogger() called before you initialize() the Omnipay gateway, plus setEnvironment() explicitly when running tests. Alternatively instantiate braintree_php directly for a one-off reproduction.Omnipay\Braintree\Message\AbstractRequest, implement getData(), register a createXxxRequest() method on the Gateway subclass. Reuse the driver's AbstractResponse for consistent success/error mapping rather than rolling your own.purchase() on timeout you can double-charge — persist an intent row first, then reconcile via Braintree's transaction search in a queued job.How can I help you explore Laravel packages today?