authnet_core 1.0.2
authnet_core: ^1.0.2 copied to clipboard
Authorize.Net SDK for Dart/Flutter: card, eCheck, PayPal payments, CIM profiles, ARB subscriptions, reporting. Not affiliated with or endorsed by Authorize.Net or Visa.
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.
Why use it #
- The broad Authorize.Net surface most applications need (payments, eCheck, PayPal, CIM, ARB, reporting, Account Updater, and hosted/tokenized flows) in one null-safe API.
- Typed requests, results, filters, sorting, and pagination instead of raw JSON maps.
- Payment-safe networking: ambiguous mutations are not retried, secrets are masked, and malformed responses fail closed.
- 160/160 pub points, 400 automated tests, and 99%+ production-line coverage.
Install #
dart pub add authnet_core
Or add it manually:
dependencies:
authnet_core: ^1.0.2
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.
