authnet_core 1.0.0 copy "authnet_core: ^1.0.0" to clipboard
authnet_core: ^1.0.0 copied to clipboard

Pure-Dart engine for the Authorize.Net API: config, typed models, request/response handling, and an exception hierarchy. Not affiliated with or endorsed by Authorize.Net or Visa.

authnet_core #

License: MIT Dart SDK

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 transactionKey server-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.

1
likes
0
points
217
downloads

Publisher

unverified uploader

Weekly Downloads

Pure-Dart engine for the Authorize.Net API: config, typed models, request/response handling, and an exception hierarchy. Not affiliated with or endorsed by Authorize.Net or Visa.

Repository (GitHub)
View/report issues

Topics

#authorize-net #payments #ecommerce

License

unknown (license)

Dependencies

http

More

Packages that depend on authnet_core