authnet_core
The pure-Dart engine of the authnet_dart SDK: config, typed models, request building, response parsing, and a typed exception hierarchy for Authorize.Net's Transaction/JSON API. No Flutter dependency; runs on server, Flutter, and web.
Not affiliated with, endorsed by, or certified by Authorize.Net or Visa.
Install
dependencies:
authnet_core: ^1.0.0
Quick start: charging a card
import 'package:authnet_core/authnet_core.dart';
final client = AuthNetClient(
config: AuthNetConfig(
apiLoginId: 'your-api-login-id',
transactionKey: 'your-transaction-key',
environment: AuthNetEnvironment.sandbox, // switch to .production when ready
),
);
final result = await client.charge(
PaymentRequest(
amount: 19.99,
method: PaymentMethod.creditCard,
billing: BillingDetails(firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com'),
card: CardDetails(number: '4111111111111111', expMonth: '12', expYear: '2030', cvv: '900'),
),
);
if (result.isApproved) {
print('Approved: ${result.transactionId}');
} else {
// pendingCustomerAction / declined / heldForReview / error / failed:
// result.message is always safe to show; charge() never throws.
print('${result.status}: ${result.message}');
}
charge() never throws: every outcome, including a network failure, comes
back as a TransactionResult you can log or display directly.
eCheck (bank account)
final result = await client.charge(
PaymentRequest(
amount: 50.00,
method: PaymentMethod.bankAccount,
billing: BillingDetails(firstName: 'Jane', lastName: 'Doe'),
bank: BankDetails(
accountType: BankAccountType.checking,
routingNumber: '071000013',
accountNumber: '123456789',
nameOnAccount: 'Jane Doe',
),
),
);
if (result.isPendingSettlement) {
// eCheck "approved" means submitted, not settled: funds move over
// 3-5 business days and can still be returned. Don't treat this as final.
}
Accept.js / Accept Hosted nonce
If you're tokenizing card data on the client (recommended for any app that touches user devices, see the root README's security model), charge the resulting nonce the same way:
final result = await client.charge(
PaymentRequest(
amount: 19.99,
method: PaymentMethod.opaqueData,
billing: BillingDetails(firstName: 'Jane', lastName: 'Doe'),
opaqueData: OpaqueData(dataDescriptor: descriptor, dataValue: nonce),
),
);
PayPal redirect payments
Start a PayPal authorization or sale by using PaymentMethod.payPal. An
accepted initial response has status: pendingCustomerAction and exposes the
PayPal URL through secureAcceptanceUrl. After PayPal returns a payer id,
finish with continuePayPal(); getPayPalDetails() can retrieve the payer
details for an existing transaction.
final pending = await client.charge(PaymentRequest(
amount: 19.99,
method: PaymentMethod.payPal,
payPal: const PayPalDetails(
successUrl: 'https://example.com/paypal/success',
cancelUrl: 'https://example.com/paypal/cancel',
),
));
// Open pending.secureAcceptanceUrl for the customer, then use the payer id
// returned by PayPal/Authorize.Net:
final completed = await client.continuePayPal(
transactionId: pending.transactionId!,
payerId: payerId,
);
The callback values must be absolute HTTPS URLs. They also need to route back to a page or app-link handler your customer can actually reach; passing syntax validation alone does not make a localhost or private-network URL usable.
Saved payment methods (CIM)
// Save the card just used, to charge again later without re-collecting it.
final saved = await client.saveMethodFromTransaction(transactionId: result.transactionId!);
// List a customer's saved cards/bank accounts.
final cards = await client.getSavedMethods(saved.customerProfileId!);
// Charge a saved method without touching raw card data again.
final again = await client.chargeSavedMethod(
customerProfileId: saved.customerProfileId!,
paymentProfileId: saved.customerPaymentProfileId!,
amount: 19.99,
);
Unlike charge(), the CIM methods (getSavedMethods, getProfileByMerchantId,
saveMethodFromTransaction) throw a typed AuthNetException on failure
(AuthNetNetworkException, AuthNetApiException, AuthNetParseException,
AuthNetConfigException): a lookup that can't complete needs to be
distinguishable from a lookup that legitimately found nothing. The one
exception: a profile that simply doesn't exist yet returns an empty result
rather than throwing, since that's a normal, expected state.
Authorize now, capture later
final auth = await client.authOnly(
PaymentRequest(
amount: 19.99,
method: PaymentMethod.creditCard,
billing: BillingDetails(firstName: 'Jane', lastName: 'Doe'),
card: CardDetails(number: '4111111111111111', expMonth: '12', expYear: '2030', cvv: '900'),
),
);
// ...later, capture the full amount (or pass a smaller `amount` to
// partially capture):
final captured = await client.priorAuthCapture(transactionId: auth.transactionId!);
Capture with an external auth code
final result = await client.captureOnly(
CaptureOnlyRequest(
amount: 19.99,
authCode: 'AUTH123', // obtained outside Authorize.Net, e.g. a terminal
card: CardDetails(number: '4111111111111111', expMonth: '12', expYear: '2030', cvv: '900'),
),
);
Refund or void
// A SETTLED transaction: refund it. An UNSETTLED one: void it instead;
// refunding an unsettled transaction fails (Authorize.Net error code 54).
final refunded = await client.refund(
RefundRequest(
transactionId: originalTransactionId,
amount: 19.99,
card: RefundCardInfo(cardNumber: '1111'), // last 4 digits are enough
),
);
final voided = await client.voidTransaction(originalTransactionId);
authOnly, priorAuthCapture, captureOnly, refund, and voidTransaction
all follow charge()'s never-throws contract: every outcome, including a
validation failure like a missing transaction id, comes back as a
TransactionResult.
Full customer profile management (CIM)
Beyond the saved-payment-methods calls above, authnet_core covers the full
Customer Information Manager surface (profiles, payment profiles, and
shipping addresses) for building an account/wallet management UI:
// Customer profiles
final profileId = await client.createCustomerProfile(
CreateCustomerProfileRequest(merchantCustomerId: 'user-42', email: 'jane@example.com'),
);
final allIds = await client.getCustomerProfileIds();
await client.updateCustomerProfile(customerProfileId: profileId, description: 'VIP');
await client.deleteCustomerProfile(profileId);
// Payment profiles: card, bank, or an Accept.js/Hosted nonce
final paymentProfileId = await client.createCustomerPaymentProfile(
CreateCustomerPaymentProfileRequest(
customerProfileId: profileId,
opaqueData: OpaqueData(dataDescriptor: descriptor, dataValue: nonce),
),
);
final method = await client.getCustomerPaymentProfile(
customerProfileId: profileId,
customerPaymentProfileId: paymentProfileId,
);
await client.validateCustomerPaymentProfile(
customerProfileId: profileId,
customerPaymentProfileId: paymentProfileId,
);
// Shipping addresses
final addressId = await client.createCustomerShippingAddress(
customerProfileId: profileId,
address: ShippingAddress(firstName: 'Jane', lastName: 'Doe', city: 'Springfield'),
);
Like the other CIM calls, every method here throws a typed AuthNetException
on failure rather than returning a result object; see "Saved payment
methods (CIM)" above for why.
getCustomerPaymentProfileNonce() needs a connectedAccessToken, an
OAuth-style token from Authorize.Net's Partner API, not your
apiLoginId/transactionKey. If you're a normal merchant calling this
on your own account, you very likely don't have one; see its dartdoc.
Transaction reporting
final detail = await client.getTransactionDetails(transactionId);
if (detail.isSettled) {
// funds have actually cleared: see the eCheck note above for why an
// "approved" charge() result alone isn't enough to know this.
}
final inBatch = await client.getTransactionList(batchId);
final forCustomer = await client.getTransactionListForCustomer(customerProfileId: profileId);
final unsettled = await client.getUnsettledTransactionList(
orderBy: TransactionListOrderBy.submitTimeUtc,
paging: const AuthNetPaging(limit: 100, offset: 1),
);
final held = await client.getHeldTransactionList();
transactionStatus on both types is Authorize.Net's raw status string
("settledSuccessfully", "capturedPendingSettlement", "FDSPendingReview",
...), not an enum, since this SDK isn't confident it knows every possible
value. isSettled/isHeldForReview check the specific values it is
confident about.
Batch and merchant reporting
final batches = await client.getSettledBatchList(
firstSettlementDate: DateTime.utc(2026, 8, 1),
lastSettlementDate: DateTime.utc(2026, 8, 20), // range capped at 31 days
);
final stats = await client.getBatchStatistics(batches.first.batchId);
for (final s in stats.statistics) {
print('${s.accountType}: ${s.chargeCount} charges, ${s.chargeAmount}');
}
final merchant = await client.getMerchantDetails();
// merchant.publicClientKey is what Accept.js/Accept Hosted need client-side.
// Proactive card-update campaigns:
final expiringSoon = await client.findPaymentProfilesExpiringInMonth('2026-09');
Recurring billing and Account Updater
createSubscription() accepts raw card/bank details. Prefer
createSubscriptionFromProfile() once a customer already has a saved CIM
payment profile. getSubscriptionList() supports every documented ARB search
type plus typed sorting/paging, and getSubscription() includes schedule,
profile, masked payment, order, and billing-attempt detail.
Account Updater merchants can use getAccountUpdaterJobSummary() and
getAccountUpdaterJobDetails(). Detail results distinguish updates from
deletions and expose the documented old/new/deleted masked card records.
findPaymentProfilesExpiringInMonth() covers
getCustomerPaymentProfileListRequest completely: Authorize.Net's schema
defines only one searchType for this operation, cardsExpiringInMonth.
There's no broader cross-customer search this SDK is missing.
Network reliability and cleanup
final client = AuthNetClient(
config: config,
retryPolicy: const RetryPolicy(maxAttempts: 3), // read-only calls only
);
// ...when you're done with this client (e.g. app shutdown, request scope ending):
client.close();
AuthNetClient applies retry policy only to semantically read-only lookups.
Charges, captures, refunds, voids, profile/subscription mutations, and token
creation are never retried automatically. package:http cannot prove from a
ClientException whether request bytes were sent, and Authorize.Net has no
idempotency key, so retrying an ambiguous mutation could duplicate a charge
or account change. Timeouts and non-200 responses are never retried either.
Pass RetryPolicy.none to disable even read-only retries.
close() releases the underlying HTTP connection pool: call it when
you're done with a client, the same way you'd close any http.Client. If
you passed your own httpClient to the constructor, close() leaves it
open, since you're responsible for a client you provided (you might be
reusing it elsewhere).
Logging
Pass onLog to see masked request/response bodies:
final client = AuthNetClient(config: config, onLog: print);
Every logged request/response is passed through maskForLog(). It redacts
the API Login ID, Transaction Key, partner access token, cardCode, bank
routingNumber/accountNumber, and opaque dataValue; cardNumber keeps only its last
four digits. Other business/customer fields can remain, so these logs still
need your normal access controls and retention policy.
Error model
TransactionStatus |
Meaning |
|---|---|
approved |
responseCode 1 with a real transaction id |
pendingCustomerAction |
PayPal responseCode 5 with a customer redirect URL |
declined |
responseCode 2 |
heldForReview |
responseCode 4: fraud filter held it |
error |
responseCode 3, a request-level API error, or Test Mode (transId "0") |
failed |
No usable response: config/parse failure, or a network failure whose server-side outcome may be unknown; verify state before retrying |
See the full API reference on pub.flutter-io.cn
(dartdoc on every public member) and example/ for a runnable
end-to-end example.
Performance
This is a network client: Authorize.Net's own round-trip (typically
200–800ms) dominates real-world latency, not anything in this package.
Measured with benchmark/response_parsing_benchmark.dart
(run it yourself with dart run benchmark/response_parsing_benchmark.dart):
| Operation | Time |
|---|---|
| Parse a typical transaction-detail response | ~23 µs |
| Parse a 2-item transaction list | ~3 µs |
| Parse a synthetic 1,000-item transaction list | ~1.6 ms |
maskForLog() a typical response body |
~6.5 µs |
RetryPolicy.delayFor() (backoff computation) |
~0.1 µs |
Parsing scales linearly with response size; even a 1,000-row report parses in under 2ms, a rounding error next to the network call that fetched it.
Security
- Keep
transactionKeyserver-side. See the root README for the three usage modes and their tradeoffs, and SECURITY.md for the full security model and PCI-DSS scope notes.
License
MIT: see LICENSE. This library does not certify PCI-DSS compliance.
Libraries
- authnet_core
- A pure-Dart client for the Authorize.Net Transaction/JSON API: config, typed models, request building, response parsing, and a typed exception hierarchy. Runs on server, Flutter, and web.