authnet_server 1.0.2
authnet_server: ^1.0.2 copied to clipboard
Authorize.Net webhooks for Dart servers: HMAC-SHA512 verification, management, notification history, retries. Not affiliated with or endorsed by Authorize.Net or Visa.
authnet_server #
The backend layer of the authnet_dart
SDK for Authorize.Net:
webhook
signature verification and webhook management for a Dart backend. Built
on top of authnet_core.
Not affiliated with, endorsed by, or certified by Authorize.Net or Visa.
Why use it #
- HMAC-SHA512 verification, typed events, webhook CRUD/testing, notification history, retry logs, and delivery payloads in one backend package.
- Fail-closed parsing and timing-safe signature comparison.
- 160/160 pub points, 65 automated tests, and 99%+ production-line coverage.
Why this is a separate package #
Webhooks are a different service from the Transaction/JSON API
authnet_core talks to: a different base URL, a different auth scheme
(HTTP Basic instead of a merchantAuthentication body), and a security
concern (signature verification) that only matters on a server that
receives inbound requests. Keeping it separate means a Flutter app that
only ever sends requests to Authorize.Net doesn't pull in webhook-handling
code it will never use.
Install #
dart pub add authnet_server
Or add both packages manually:
dependencies:
authnet_core: ^1.0.2
authnet_server: ^1.0.2
Verifying a webhook (do this first, always) #
Authorize.Net signs every webhook delivery with your
Signature Key,
sent as the X-ANET-Signature header. Verify it before you trust anything in the
body: this is the only thing standing between your webhook endpoint and
anyone on the internet who finds its URL and sends it a forged payload.
import 'package:authnet_server/authnet_server.dart';
// Inside your HTTP handler (shown here as pseudocode, adapt to whatever
// server framework you're using: shelf, package:dart_frog, etc.):
Future<Response> handleWebhook(Request request) async {
final rawBody = await request.readAsString(); // the RAW body: see the warning below
final signatureHeader = request.headers['X-ANET-Signature'] ?? '';
final isValid = verifyWebhookSignature(
rawBody: rawBody,
signatureHeader: signatureHeader,
signatureKey: yourSignatureKey,
);
if (!isValid) {
return Response(401, body: 'Invalid signature');
}
final event = parseWebhookEvent(rawBody);
// ...now it's safe to act on `event`.
}
Read the raw body, not a re-parsed/re-serialized one. Signature verification hashes the exact bytes Authorize.Net sent. If your framework parses the body into a JSON object and you re-encode it before verifying, whitespace or key-order differences will make a completely legitimate webhook fail verification. Read the raw string/bytes first, verify, then parse.
The HMAC-SHA512 implementation, including hex-decoding the 128-character Signature Key into binary the way Authorize.Net documents, is checked against an independently-computed reference vector. A live sandbox delivery wasn't available for this release, so test one yourself to confirm your HTTP framework preserves the exact raw body and header end to end.
Handling the event #
final event = parseWebhookEvent(rawBody);
// Authorize.Net's webhook delivery is at-least-once, not exactly-once;
// track notificationId to avoid double-processing a retried delivery:
if (await alreadyProcessed(event.notificationId)) return Response(200);
switch (event.eventType) {
case WebhookEventTypes.authCaptureCreated:
final transactionId = event.entityId;
if (transactionId != null) {
// Look up the full, trustworthy detail rather than relying on
// whatever fields happen to be in the webhook payload:
final detail = await authNetClient.getTransactionDetails(transactionId);
// ...update your order/subscription records...
}
case WebhookEventTypes.fraudHeld:
// ...notify someone to review it...
}
await markProcessed(event.notificationId);
return Response(200);
Managing webhooks #
final webhookClient = WebhookClient(
config: WebhookConfig(
apiLoginId: 'your-api-login-id',
transactionKey: 'your-transaction-key',
signatureKey: 'your-signature-key',
),
);
final webhook = await webhookClient.createWebhook(
name: 'Payment events',
url: 'https://your-server.example.com/webhooks/authorize-net',
eventTypes: [
WebhookEventTypes.authCaptureCreated,
WebhookEventTypes.refundCreated,
WebhookEventTypes.voidCreated,
WebhookEventTypes.fraudHeld,
],
);
final all = await webhookClient.listWebhooks();
await webhookClient.updateWebhook(webhookId: webhook.webhookId, status: WebhookStatus.inactive);
await webhookClient.pingWebhook(webhook.webhookId); // inactive webhooks only
final failures = await webhookClient.listNotificationHistory(
deliveryStatus: WebhookDeliveryStatus.failed,
limit: 100,
);
if (failures.isNotEmpty) {
final detail = await webhookClient.getNotification(
failures.first.notificationId,
);
// Inspect detail.retryLogs and detail.payload.
}
await webhookClient.deleteWebhook(webhook.webhookId);
webhookClient.close(); // when you're done with it
The callback must be a publicly reachable HTTPS URL. Authorize.Net documents that webhook URLs may contain only letters, numbers, dots, hyphens, underscores, and path slashes, so query strings, fragments, embedded credentials, custom ports, localhost URLs, and other special characters are rejected locally. HTTPS alone does not guarantee that Authorize.Net can reach the endpoint; verify a real sandbox delivery before relying on it.
Every WebhookClient method throws a typed WebhookException on failure
(WebhookApiException carries the HTTP status code and Authorize.Net's
error message; WebhookConfigException for missing credentials), so a
failed webhook management call is always visible to whoever's setting it
up rather than returning a silent empty result.
For create/update/delete, a timeout or connection failure means the remote outcome is unknown. The exception message tells you to query current webhook state before retrying so an ambiguous mutation is not repeated blindly.
parseWebhookEvent() similarly throws a typed WebhookParseException for
malformed input rather than leaking a bare FormatException.
listWebhooks() and listEventTypes() have both been run against a live
account, including checking that every one of WebhookEventTypes' 23
constants matches Authorize.Net's real event type names exactly.
createWebhook() was not. The account available for this release can't
create a webhook at all, through three independent paths: this SDK's
createWebhook(), a request built by hand to match Authorize.Net's
documented shape exactly, and Authorize.Net's own Merchant Interface UI,
tried against two different real endpoint URLs. All three fail with the
same generic, unhelpful error, which rules out a malformed request or a
bad endpoint URL. What's left is an account-level restriction
Authorize.Net hasn't enabled on this account, which only their support
team can confirm or fix.
If createWebhook() fails for you too, try creating a webhook through the
sandbox Merchant Interface UI first. If
that also fails, it's an account issue, not this SDK, and Authorize.Net
support is the right next step.
getWebhook()/updateWebhook()/deleteWebhook()/pingWebhook() were
consequently never exercised against a real webhook either. Notification
history may also be empty until a webhook has received a real delivery. Their
field mappings and filters follow Authorize.Net's published REST examples;
parsers reject malformed required fields instead of manufacturing incomplete
objects.
License #
MIT: see LICENSE. This library does not certify PCI-DSS compliance.
