omnipay/authorizenet
Authorize.Net payment gateway driver for the Omnipay PHP library. Process card payments with authorize, capture, purchase, refund and void operations through a consistent, framework-agnostic API, with support for Authorize.Net's AIM and SIM integration methods.
Install the version that matches your Omnipay core. The package ships two incompatible majors:
# Omnipay v3 (PHP 7.1+, PSR-4) — use this for new work
composer require omnipay/common:^3.0 omnipay/authorizenet:^3.0
# Omnipay v2 (legacy Laravel 5.x apps)
composer require omnipay/common:^2.0 omnipay/authorizenet:^2.0
Know which gateway you actually need — this package ships five, and they do not share a feature set:
| Short name | Class | Use for |
|---|---|---|
AuthorizeNet_AIM |
AIMGateway |
Simple server-to-server charge/auth. Card data hits your server (PCI scope). |
AuthorizeNet_CIM |
CIMGateway |
Stored customer profiles, saved cards, repeat/subscription charges. |
AuthorizeNet_Api |
ApiGateway |
The modern JSON API + Accept.js tokenisation + webhooks (acceptNotification). |
AuthorizeNet_DPM / AuthorizeNet_SIM |
DPMGateway / SIMGateway |
Hosted/relay payment forms. |
Where to look first. The README is thin; the request classes under src/Message/<Gateway>/ are the real documentation — they enumerate every supported parameter. Start with Message/AIM/AIMPurchaseRequest.php, then Message/Api/ApiAbstractRequest.php.
First use case — a sandbox AIM purchase:
$gateway = Omnipay\Omnipay::create('AuthorizeNet_AIM');
$gateway->setApiLoginId(config('services.authorizenet.login_id'));
$gateway->setTransactionKey(config('services.authorizenet.transaction_key'));
$gateway->setTestMode(true); // switches endpoint only
$gateway->setDeveloperMode(true); // AIM: verbose/test responses in sandbox
$response = $gateway->purchase([
'amount' => '10.00',
'currency' => 'USD',
'card' => [
'number' => '4111111111111111',
'expiryMonth' => '12',
'expiryYear' => '2030',
'cvv' => '123',
'billingFirstName' => 'Jane',
'billingLastName' => 'Doe',
],
])->send();
Credentials come from the Authorize.Net merchant interface (Account → API Login ID and Transaction Key). Sandbox credentials are separate — create them at sandbox.authorize.net; production keys will not authenticate against the test endpoint.
Bind one gateway in the container, inject the interface. Never call Omnipay::create() in controllers; build it once in a service provider and type-hint Omnipay\Common\GatewayInterface so tests can swap a stub.
$this->app->singleton(GatewayInterface::class, fn () => tap(
Omnipay::create('AuthorizeNet_Api'),
function ($g) {
$g->setApiLoginId(config('services.authorizenet.login_id'));
$g->setTransactionKey(config('services.authorizenet.transaction_key'));
$g->setTestMode(app()->isLocal());
}
));
Persist the whole response, not just success/failure. Every attempt should write getTransactionReference(), getCode(), getMessage(), and getData() to your payments table. You will need the transaction reference weeks later for capture/void/refund and for reconciliation against the merchant interface.
Two-step authorise → capture is the standard pattern when you ship goods later:
$auth = $gateway->authorize([...])->send();
if ($auth->isSuccessful()) {
Order::find($id)->update([
'auth_txn' => $auth->getTransactionReference(),
'auth_code' => $auth->getAuthorizationCode(),
]);
}
// ... later, at shipment
$capture = $gateway->capture([
'amount' => '10.00',
'transactionReference' => $order->auth_txn,
])->send();
Void vs refund is a timing decision, not a preference. Before the daily batch settles (roughly 16:00–17:00 Pacific), use void() with the original transaction reference. After settlement, void() fails and you must use refund(). Build a small helper that tries void, checks the response code, and falls back to refund — or simply decide based on age of the transaction.
Saved cards belong in CIM (or Accept.js), never in purchase() loops. CIM splits the flow:
// 1. Store the customer (returns a profile id)
$profile = $gateway->createCustomerProfile([
'email' => $user->email,
'card' => [...],
'billingAddress1' => ...,
])->send();
// 2. Charge the stored profile — no card data on the wire
$charge = $gateway->purchase([
'amount' => '25.00',
'customerProfileId' => $user->cim_profile_id,
'customerPaymentProfileId' => $user->cim_payment_profile_id,
])->send();
A CIM purchase will ignore a card array — it needs both profile IDs, or it errors. Use createCustomerProfileFromTransaction to retro-fit a profile onto an existing AIM sale.
Hosted forms follow a different lifecycle. DPM/SIM purchase() returns isRedirect() === true; the amount is pre-set and the card is entered on Authorize.Net's page. You render $response->getRedirectData() as hidden inputs POSTed to $response->getRedirectUrl(), then handle the callback with completePurchase() (DPM) / completeAuthorize() (SIM). Use these when PCI scope matters more than UX.
Accept.js is the low-friction PCI path. Let the browser tokenise the card, then send only the opaque token to the Api gateway — a purchase(['opaqueData' => [...]]) call. Inspect ApiAbstractRequest in your installed version for the exact key names, and use the Api gateway's acceptNotification() to consume webhooks for async settlement states.
Testing. Don't mock HTTP; either run against the sandbox in CI (with a PaymentAttempt guard so re-runs don't duplicate-charge within the 2-minute window) or construct responses directly: new Omnipay\AuthorizeNet\Message\AIM\Response($request, ['responseCode' => 1, ...]). Test cards: 4111111111111111 (approved), 4222222222222 (declined), and vary amount last-digits to trigger error code 3.
setTestMode(true) only changes the endpoint URI. It does not make bad cards pass, and it does not enable verbose diagnostics — that's setDeveloperMode(true) on AIM. Both must be env-driven, and neither should ever be true in production.payment_uuid and reconcile rather than retrying.send() can throw InvalidResponseException on transport/parse failure (WAFs and proxies returning HTML bite here). The charge may still have gone through. Always catch around send() and mark the attempt unknown for reconciliation; never blindly retry.getCode(), not just isSuccessful(). Codes: 1 approved, 2 declined, 3 error, 4 held for review. Declines and gateway errors are both "not successful" but have completely different retry semantics — and code 4 needs a deliberate hold, not a refund.InvalidRequestException before the network call. Useful: wrap construction in the same try/catch and log the input keys (card number excluded).'10.00', not 10.0 or a float cast from a calculation. Format with number_format($cents / 100, 2, '.', ''). Currency is USD-only for Authorize.Net.$parameters['card'] contains the PAN and CVV. Log getTransactionReference(), the response getData(), and the last-4 from the response payload instead.$gateway->setHttpClient($client) accepts a Guzzle client, so you can attach logging middleware, set timeouts, and enable retries on connection errors only (not on 5xx from the gateway).ApiAbstractRequest and register a custom request via AbstractGateway::createRequest() when you need an endpoint the package doesn't wrap (e.g. transaction detail lookups), and use $gateway->supportsPurchase() / supportsRefund() for feature detection rather than instanceof checks.authorizenet/authorizenet SDK.How can I help you explore Laravel packages today?