nti/authorizenet-bundle
Integrate Authorize.Net payments into Symfony/PHP applications with this bundle. It simplifies API setup for card charges, customer profiles, subscriptions, and transaction handling, helping developers add secure payment flows with less boilerplate.
This is a Symfony bundle, not a Laravel package — there is no service provider, no auto-discovery, and no config/packages convention that Laravel will pick up. The first decision is whether you actually want to introduce symfony/framework-bundle into a Laravel app just for a payment client (usually you don't) or whether you're working in a Symfony service that happens to be called from Laravel. If it's the latter, pull the underlying authorizenet/authorizenet SDK in directly and skip this bundle.
Assuming you're in a Symfony app:
composer require nti/authorizenet-bundle authorizenet/authorizenet (the bundle does not pin the SDK transitively in all versions, so check composer why authorizenet/authorizenet). Latest release at time of writing is 1.1.4 — pin it exactly.config/bundles.php:
Nti\AuthorizeNetBundle\NtiAuthorizeNetBundle::class => ['all' => true],
DependencyInjection/Configuration.php first — it's the source of truth for the exact key names, and they have shifted between releases:
# config/packages/nti_authorize_net.yaml
nti_authorize_net:
environment: sandbox # or "production"
api_credentials:
login_id: '%env(AUTHNET_LOGIN_ID)%'
transaction_key: '%env(AUTHNET_TRANSACTION_KEY)%'
vendor/nti/authorizenet-bundle/Resources/config/services.yaml — this is the only reliable place to learn what the bundle actually registers. Expect something along the lines of nti.authorize_net.service.aim, …service.cim, and …service.arb wrapping the legacy AuthorizeNetAIM / AuthorizeNetCIM / AuthorizeNetARB classes.First use case — a single card charge (AIM), with proper exception handling:
use Nti\AuthorizeNetBundle\Exception\ANetRequestException;
use Nti\AuthorizeNetBundle\Service\AimService;
public function __construct(private AimService $aim) {}
public function charge(int $cents, string $opaqueCard): array
{
$this->aim->setAmount(number_format($cents / 100, 2, '.', ''));
$this->aim->setCardNum($opaqueCard);
$this->aim->setCardExp('2512');
$this->aim->setDescription('Order #1042');
try {
$response = $this->aim->authorizeAndCapture();
} catch (ANetRequestException $e) {
// As of 1.1.4 the exception separates getCode() (gateway/transport
// code) from getMessage() (human-readable reason) for CC requests.
// Log code structurally; surface message to the user only if safe.
return [
'ok' => false,
'code' => $e->getCode(),
'message' => $e->getMessage(),
];
}
return [
'ok' => $response->getResponseCode() === '1', // APPROVED
'transId' => $response->getTransactionId(),
'message' => $response->getResponseReasonText(),
];
}
Note the return is the legacy AuthorizeNetAIM_Response, not the newer net\authorize\api\contract\v1 objects — the two SDK styles coexist in the Authorize.Net PHP ecosystem and this bundle predates/wraps the older one. That's the single biggest source of confusion when following Authorize.Net's current docs.
Isolate the bundle behind your own interface. Because this bundle has zero stars, zero dependents, and a very thin release history, treat it as a replaceable adapter, not a foundation:
interface PaymentGateway
{
public function charge(Money $amount, CardToken $card): ChargeResult;
}
final class AuthorizeNetGateway implements PaymentGateway
{
public function __construct(private AimService $aim) {}
public function charge(Money $amount, CardToken $card): ChargeResult
{
// normalize errors, map response codes AND exception codes, never leak vendor objects upward
}
}
When the bundle inevitably breaks on a future Symfony major, you rewrite one class.
Handle the exception path explicitly. ANetRequestException is thrown for transport/request failures (timeouts, malformed card data, gateway rejects). Since 1.1.4 separates the code from the message for CC requests, you can now branch on getCode() for programmatic handling (e.g. distinguishing a declined-vs-transport failure) while keeping getMessage() for logs/user-facing copy. Before 1.1.4 the code was often folded into the message string — if you're upgrading, audit any code that parsed the message:
try {
$response = $this->aim->authorizeAndCapture();
} catch (ANetRequestException $e) {
$code = $e->getCode(); // 1.1.4+: structured code, use this
$text = $e->getMessage(); // human-readable reason
// don't strpos($text, 'declined') anymore — check $code
}
Match the service to the job. AIM for one-shot charges, CIM for stored customer profiles (create profile → createCustomerProfileFromTransaction → charge the profile later), ARB for recurring subscriptions. Don't try to build subscriptions out of loops of AIM charges — you lose the retry/dunning semantics and the card-updater that ARB provides.
Normalize the response before it touches your domain. The legacy response object mixes strings, mixed ints, and occasionally null:
$resultCode = $response->getResponseCode(); // '1' approved, '2' declined, '3' error, '4' held
$approved = $resultCode === '1';
$declined = $resultCode === '2'; // safe to surface to the customer
// everything else is a gateway/transport failure → throw, log, alert
Test against the sandbox with Authorize.Net's published test card numbers, and keep sandbox creds in a separate env block so nobody accidentally ships environment: sandbox to prod. A config assertion on boot (APP_ENV=prod must imply environment: production) has caught this more than once.
Mock the bundle service, not the network. In unit tests, bind your PaymentGateway interface to a stub. Integration tests hit the sandbox and assert on transaction_id round-tripping via getTransactionDetailsRequest. Add a test that forces an ANetRequestException to prove your getCode()/getMessage() handling survives a future bundle bump.
Extension/CompilerPass machinery does nothing. You'd be calling $bundle->boot() by hand. Just use the SDK."nti/authorizenet-bundle": "1.1.4"), audit it for composer audit, and have a plan to fork or swap. The Authorize.Net SDK itself is what you actually depend on; the bundle is a thin service-definition layer.ANetRequestException now exposes the code and message as separate values for CC requests. If you were parsing message strings (e.g. regexing out a code), migrate to getCode(). Message format may differ from earlier versions, so log both if you forward to an error tracker and want historical comparability.composer.json likely caps symfony/framework-bundle at ^4.4 || ^5.0. On Symfony 6/7 you'll hit Conclusion: don't install and need to fork with a widened constraint or use --ignore-platform-reqs (don't — the ContainerAwareCommand APIs really did change).if ($response->getResponseCode()) is truthy for '2' (declined) and '3' (error). Always compare against explicit '1'. Exception codes are typically numeric — don't confuse the two namespaces.authorizeAndCapture vs authorizeOnly. The former is what you want for most checkouts; if you use authorizeOnly you must have a capture job, or held authorizations will expire and silently release funds. There's no scheduler in the bundle.number_format($cents/100, 2, '.', '') — passing a float like 10.1 can serialize as 10.1 and get rejected or, worse, silently rounded differently than your ledger.curl timeout. Wrap charges with your own idempotency key stored in a DB row before calling the gateway, and reconcile via getTransactionDetailsRequest on timeout rather than retrying blindly.var_dump($response->getResponseReasonCode(), $response->getResponseReasonText()) — the reason text is far more informative than the code alone, and Authorize.Net's docs list the numeric reason codes. Also dump $response->getTransactionId() immediately; it's your only handle on the transaction. On exceptions, log getCode() and getMessage() separately (1.1.4+) — collapsing them loses the structured signal.Resources/config/services.yaml from your app by registering services with the same IDs under services.yaml (Symfony last-wins for aliases, but constructor args need a compiler pass in some cases), or simply decorate. The bundle exposes no events — if you need hooks (e.g. "on decline, notify"), wrap the service as above rather than patching vendor.How can I help you explore Laravel packages today?