sondahub

sondahub / Authorize.net sandbox

An Authorize.net mock API that checks your requests like the real one

Authorize.net’s API, answered by sondahub: XML or JSON checked against the gateway’s schema, element order included, the testing guide’s cards and triggers, batches that settle every minute so refunds and reports work, customer profiles, subscriptions, Accept nonces and a hosted payment form, and webhooks signed so your verification code can be proven.

An independent imitation for testing. Not affiliated with, or endorsed by, Authorize.net, Visa or Cybersource. Nothing is charged.

Connect

Instead of
https://apitest.authorize.net/xml/v1/request.api
Use
https://api.sondahub.com/xml/v1/request.api
API Login ID
any, up to 25 characters — sondahub
Transaction Key
any 16 characters — SandboxKey123456
Signature Key
B1224AC959AB2E41CF56404EBDC5677F804E9CDD88DA26E9D8DEB2B91E8466F116F66A580DC0120ABB662C672C4C76D51E360B893327D077FD6EFA95F5EA439C
Webhooks REST
https://api.sondahub.com/rest/v1 (Basic: login and key)
Hosted form
https://api.sondahub.com/sandbox/authorizenet/payment/payment
OpenAPI 3
https://api.sondahub.com/sandbox/authorizenet/openapi.json

Import it. In Sonda: Import → From a URL, paste the OpenAPI address. The real API is one endpoint every call is posted to, so the import gives each call — and each kind of transaction — a path of its own after /xml/v1/request.api: sondahub’s labels, since the sandbox reads only the body. Each request comes with a body in schema order, the demo credentials in merchantAuthentication and ids from the seed account, so they work as they are. The webhooks requests take Basic auth: sondahub / SandboxKey123456.

Authorize.net’s SDKs know nothing about sondahub’s sessions, so give them the few lines that point them here and carry X-Sondahub-Session from one answer to the next request — otherwise every call starts from the seed account and the payment you just made is not there to capture.

Python (authorizenet)

from decimal import Decimal
import requests
from authorizenet import apicontractsv1
from authorizenet.apicontrollers import createTransactionController

# send the SDK to the sandbox, and carry its session from each answer to the next request
session = {}
_post = requests.post
def _sandbox(url, data=None, headers=None, **kw):
    url = url.replace('https://apitest.authorize.net', 'https://api.sondahub.com')
    r = _post(url, data=data, headers={**(headers or {}), **session}, **kw)
    if 'X-Sondahub-Session' in r.headers:
        session['X-Sondahub-Session'] = r.headers['X-Sondahub-Session']
    return r
requests.post = _sandbox

auth = apicontractsv1.merchantAuthenticationType()
auth.name, auth.transactionKey = 'sondahub', 'SandboxKey123456'
card = apicontractsv1.creditCardType()
card.cardNumber, card.expirationDate = '4111111111111111', '2030-12'
payment = apicontractsv1.paymentType()
payment.creditCard = card
tr = apicontractsv1.transactionRequestType()
tr.transactionType, tr.amount, tr.payment = 'authCaptureTransaction', Decimal('12.50'), payment
req = apicontractsv1.createTransactionRequest()
req.merchantAuthentication, req.transactionRequest = auth, tr

ctrl = createTransactionController(req)   # the SDK's sandbox endpoint; the hook swaps the host
ctrl.execute()
r = ctrl.getresponse()
print(r.messages.resultCode, r.transactionResponse.transId)

Node (authorizenet)

const { APIContracts: C, APIControllers: K } = require('authorizenet')
// the axios the SDK itself loads
const axios = require(require.resolve('axios', { paths: [require.resolve('authorizenet')] }))

// send the SDK to the sandbox, and carry its session from each answer to the next request
let session = null
axios.interceptors.request.use((cfg) => {
  cfg.baseURL = cfg.baseURL.replace('https://apitest.authorize.net', 'https://api.sondahub.com')
  if (session) cfg.headers.set('X-Sondahub-Session', session)
  return cfg
})
axios.interceptors.response.use((res) => {
  session = res.headers['x-sondahub-session'] ?? session
  return res
})

const auth = new C.MerchantAuthenticationType()
auth.setName('sondahub')
auth.setTransactionKey('SandboxKey123456')
const card = new C.CreditCardType()
card.setCardNumber('4111111111111111')
card.setExpirationDate('2030-12')
const payment = new C.PaymentType()
payment.setCreditCard(card)
const tr = new C.TransactionRequestType()
tr.setTransactionType(C.TransactionTypeEnum.AUTHCAPTURETRANSACTION)
tr.setAmount('12.50')
tr.setPayment(payment)
const req = new C.CreateTransactionRequest()
req.setMerchantAuthentication(auth)
req.setTransactionRequest(tr)

const ctrl = new K.CreateTransactionController(req.getJSON())   // the SDK's sandbox endpoint; the interceptor swaps the host
ctrl.execute(() => {
  const res = new C.CreateTransactionResponse(ctrl.getResponse())
  console.log(res.getMessages().getResultCode(), res.getTransactionResponse().getTransId())
})

Try it here

These examples share one session: run them in order and each sees what the one before it did.
Charge a card
curl -X POST https://api.sondahub.com/xml/v1/request.api \
  -H "Content-Type: application/json" \
  -d '{
  "createTransactionRequest": {
    "merchantAuthentication": {"name":"sondahub","transactionKey":"SandboxKey123456"},
    "refId": "order-1001",
    "transactionRequest": {
      "transactionType": "authCaptureTransaction",
      "amount": "24.00",
      "payment": {"creditCard":{"cardNumber":"4111111111111111","expirationDate":"2030-12","cardCode":"999"}},
      "order": {"invoiceNumber":"INV-1001","description":"Sandbox socks"},
      "billTo": {"firstName":"Ada","lastName":"Lovelace","address":"14 Main Street","city":"Pecan Springs","state":"TX","zip":"44628","country":"US"}
    }
  }
}'
ZIP 46282 declines: responseCode 2, E00027
curl -X POST https://api.sondahub.com/xml/v1/request.api \
  -H "Content-Type: application/json" \
  -d '{
  "createTransactionRequest": {
    "merchantAuthentication": {"name":"sondahub","transactionKey":"SandboxKey123456"},
    "transactionRequest": {
      "transactionType": "authCaptureTransaction",
      "amount": "9.00",
      "payment": {"creditCard":{"cardNumber":"4111111111111111","expirationDate":"2030-12","cardCode":"999"}},
      "billTo": {"firstName":"Ada","lastName":"Lovelace","address":"14 Main Street","city":"Pecan Springs","state":"TX","zip":"46282","country":"US"}
    }
  }
}'
amount before transactionType: E00003, as the real API answers
curl -X POST https://api.sondahub.com/xml/v1/request.api \
  -H "Content-Type: application/json" \
  -d '{
  "createTransactionRequest": {
    "merchantAuthentication": {"name":"sondahub","transactionKey":"SandboxKey123456"},
    "transactionRequest": {
      "amount": "5.00",
      "transactionType": "authCaptureTransaction",
      "payment": {"creditCard":{"cardNumber":"4111111111111111","expirationDate":"2030-12","cardCode":"999"}}
    }
  }
}'
Refund the seed’s settled payment
curl -X POST https://api.sondahub.com/xml/v1/request.api \
  -H "Content-Type: application/json" \
  -d '{
  "createTransactionRequest": {
    "merchantAuthentication": {"name":"sondahub","transactionKey":"SandboxKey123456"},
    "transactionRequest": {
      "transactionType": "refundTransaction",
      "amount": "5.00",
      "payment": {"creditCard":{"cardNumber":"1111","expirationDate":"XXXX"}},
      "refTransId": "67433922005"
    }
  }
}'
Unsettled transactions, newest first
curl -X POST https://api.sondahub.com/xml/v1/request.api \
  -H "Content-Type: application/json" \
  -d '{
  "getUnsettledTransactionListRequest": {
    "merchantAuthentication": {"name":"sondahub","transactionKey":"SandboxKey123456"},
    "sorting": {"orderBy":"submitTimeUTC","orderDescending":true},
    "paging": {"limit":"10","offset":"1"}
  }
}'
A customer profile, validated with a live zero-dollar authorization
curl -X POST https://api.sondahub.com/xml/v1/request.api \
  -H "Content-Type: application/json" \
  -d '{
  "createCustomerProfileRequest": {
    "merchantAuthentication": {"name":"sondahub","transactionKey":"SandboxKey123456"},
    "profile": {
      "merchantCustomerId": "CUST-5001",
      "description": "Grace Hopper",
      "email": "[email protected]",
      "paymentProfiles": [{"customerType":"individual","billTo":{"firstName":"Ada","lastName":"Lovelace","address":"14 Main Street","city":"Pecan Springs","state":"TX","zip":"44628","country":"US"},"payment":{"creditCard":{"cardNumber":"370000000000002","expirationDate":"2031-09"}}}]
    },
    "validationMode": "liveMode"
  }
}'
A hosted payment form token — then open the form
curl -X POST https://api.sondahub.com/xml/v1/request.api \
  -H "Content-Type: application/json" \
  -d '{
  "getHostedPaymentPageRequest": {
    "merchantAuthentication": {"name":"sondahub","transactionKey":"SandboxKey123456"},
    "transactionRequest": {
      "transactionType": "authCaptureTransaction",
      "amount": "20.00",
      "order": {"invoiceNumber":"INV-2001","description":"Workshop seat"}
    },
    "hostedPaymentSettings": {
      "setting": [{"settingName":"hostedPaymentReturnOptions","settingValue":"{\"showReceipt\": true, \"url\": \"https://example.com/receipt\", \"urlText\": \"Back to the shop\"}"}]
    }
  }
}'

From the shell, keep the token yourself: curl -i shows X-Sondahub-Session; send it back with -H "X-Sondahub-Session: …". XML works the same way, with Content-Type: application/xml and the AnetApi/xml/v1/schema/AnetApiSchema.xsd namespace. How sessions work.

Test cards and the values that trigger results

The testing guide’s cards work with any future expiration date; any other number that passes the Luhn check works too, if its brand is one Authorize.net takes. A number that fails it is reason 6, a bad date reason 7, a past one reason 8.

CardBrand
370000000000002American Express
6011000000000012Discover
3088000000000017JCB
38000000000006Diners Club / Carte Blanche
4007000000027Visa
4012888818888Visa
4111111111111111Visa
5424000000000015Mastercard
2223000010309703Mastercard (2-series)
2223000010309711Mastercard (2-series)
6221499053360818China UnionPay (processed as Discover)
6262320002000067China UnionPay (processed as Discover)
6284480000000008China UnionPay (processed as Discover)

Billing ZIP (billTo.zip)

ZIPWhat happens
46282Declines: responseCode 2, reason 2.
46201A The street address matched, but the ZIP code did not.
46203E The AVS data provided is invalid or AVS is not allowed for the card type that was used. Declines (reason 27).
46204G The card was issued by a bank outside the U.S. and does not support AVS.
46205N Neither the street address nor ZIP code matched. Declines (reason 27).
46207R AVS was unavailable at the time the transaction was processed. Retry transaction.
46208S The U.S. card issuing bank does not support AVS.
46209U The address information for the cardholder is unavailable.
46211W The nine digit ZIP code matched, but the street address did not.
46214X Both the street address and nine-digit ZIP code matched.
46217Z The ZIP code matched, but the street address did not.
any otherY with a billing address, P without one.

Card code (cardCode)

CodecvvResultCode
900M Successful match.
901N Does NOT match. Declines (reason 65).
902S Should have been present.
903U Issuer unable to process request.
904P Not processed.
any otherP

More

  • An eCheck over $100 declines; the routing number must pass the ABA check (reason 9), the account number be 5–17 digits (reason 10). Routing number 121042882 works.
  • Over $5,000.00 the sandbox’s Amount Filter authorizes the payment and holds it for review: responseCode 4, reason 252, status FDSAuthorizedPendingReview, until updateHeldTransaction approves or declines it.
  • The same charge again — type, amount, card and invoice number — within two minutes is a duplicate (responseCode 3, reason 11). The duplicateWindow setting changes the window; 0 turns it off.
  • Card 4222222222222 with an amount equal to a reason code answers that reason ($2.00 → 2, $27.00 → 27): the old test-mode trick, kept as a sandbox extra.
  • The testRequest setting validates the payment and keeps nothing: transId 0.

Statuses and settlement

A charge is capturedPendingSettlement until its batch closes, then settledSuccessfully; an authorization is authorizedPendingCapture for 30 days, then expired. The sandbox closes a batch every minute, so a capture settles about a minute after it is made — in the first batch closing at least 30 seconds later — and cards and eChecks settle in batches of their own. Prior-auth captures and voids act on the original transaction and answer with its id; a refund is a new transaction that needs a settled one (reason 54 otherwise) and can’t give back more than was captured (reason 55). Voiding a settled payment is reason 16.

getSettledBatchList answers the last 24 hours without dates, up to 31 days with them, and includeStatistics counts charges, refunds, voids, declines and errors per card type. The seed account has two weeks of daily batches (they close at 02:00 UTC) and a few open payments: an authorization of $129.00 to capture (63486784401), one to void (66973568802), a capture waiting for its batch (60460353203) and $6,250.00 held for review (63947137604).

Customer profiles and subscriptions

Customer Information Manager keeps profiles, their payment profiles — cards and bank accounts, masked on the way out (XXXX1111, XXXX for the date unless unmaskExpirationDate) — and shipping addresses, with the real service’s rules: one of merchantCustomerId, description or email; no duplicates (E00039, naming the existing id); ten payment profiles at most; nothing in use by an active subscription can be deleted. validationMode testMode checks the card; liveMode runs a zero-dollar authorization that the triggers above apply to, and answers the comma-delimited direct response. Charge a profile with profile.customerProfileId and paymentProfile.paymentProfileId, or save a card as you charge it with profile.createProfile.

Recurring Billing checks a subscription’s schedule as the real service does — start date not in the past, interval 7–365 days or 1–12 months, trial occurrences and amounts together, no duplicates (E00012) — and, paid by card or bank account, makes a customer profile for it (Profile created by Subscription: …). The sandbox keeps and reports subscriptions but doesn’t run their payments. The seed has 8 customer profiles, 11 payment profiles and 3 subscriptions.

Accept: nonces and the hosted form

securePaymentContainerRequest — what Accept.js calls from the browser — takes card details with the API Login ID and a public client key (any; getMerchantDetails hands one out) and answers opaqueData with COMMON.ACCEPT.INAPP.PAYMENT: pay with it once, within 15 minutes, as payment.opaqueData (E00114 the second time, E00115 when it has expired).

getHostedPaymentPageRequest answers a token; post it from the browser in a form field named token to https://api.sondahub.com/sandbox/authorizenet/payment/payment instead of https://test.authorize.net/payment/payment. The form is sondahub’s own: a card number, date, code and ZIP, the same checks and triggers as the API, the receipt with your hostedPaymentReturnOptions link (or straight there when showReceipt is false), and the webhooks your account had when the token was made. That payment isn’t in your session — the browser doesn’t carry your token.

Webhooks, signed

Register endpoints with the REST API — POST /rest/v1/webhooks with name, url, eventTypes and status, Basic auth with your login and key — and every call that raises one of net.authorize.payment.authorization.created, net.authorize.payment.authcapture.created, net.authorize.payment.capture.created, net.authorize.payment.refund.created, net.authorize.payment.priorAuthCapture.created, net.authorize.payment.void.created, net.authorize.payment.fraud.held, net.authorize.payment.fraud.approved, net.authorize.payment.fraud.declined, net.authorize.customer.created, net.authorize.customer.updated, net.authorize.customer.deleted, net.authorize.customer.paymentProfile.created, net.authorize.customer.paymentProfile.updated, net.authorize.customer.paymentProfile.deleted, net.authorize.customer.subscription.created, net.authorize.customer.subscription.updated, net.authorize.customer.subscription.suspended, net.authorize.customer.subscription.terminated, net.authorize.customer.subscription.cancelled, net.authorize.customer.subscription.expiring, net.authorize.customer.subscription.expired, net.authorize.customer.subscription.failed posts your URL a notification: notificationId, eventType, eventDate, webhookId and the payload (for a payment: responseCode, authCode, avsResponse, authAmount, entityName, id). The sandbox sends each one once, while it answers the call that raised it (it waits up to 3 s), and keeps the outcome in your session: GET /rest/v1/notifications lists them as Delivered or Failed, and …/notifications/{id} has the payload and the attempt. POST /rest/v1/webhooks/{id}/pings sends a sample and answers 200 when your URL answered 2xx. The subscription events that come from running payments — suspended, terminated, expiring, expired, failed — can be subscribed to but never arrive, since the sandbox doesn’t run them.

Each notification carries X-ANET-Signature: sha512=…, the HMAC-SHA512 of the raw body keyed with the Signature Key. The same key, as bytes, signs each payment’s transHashSha2 over ^login^transId^amount^:

Python

import hashlib, hmac

SIGNATURE_KEY = 'B1224AC959AB2E41CF56404EBDC5677F804E9CDD88DA26E9D8DEB2B91E8466F116F66A580DC0120ABB662C672C4C76D51E360B893327D077FD6EFA95F5EA439C'

def valid_notification(raw_body: bytes, header: str) -> bool:
    want = 'sha512=' + hmac.new(SIGNATURE_KEY.encode(), raw_body, hashlib.sha512).hexdigest()
    return hmac.compare_digest(want.lower(), (header or '').lower())

def valid_trans_hash(login: str, trans_id: str, amount: str, trans_hash_sha2: str) -> bool:
    message = f'^{login}^{trans_id}^{amount}^'          # amount as sent back: 12.50
    want = hmac.new(bytes.fromhex(SIGNATURE_KEY), message.encode(), hashlib.sha512).hexdigest()
    return hmac.compare_digest(want.upper(), trans_hash_sha2.upper())

Node

import crypto from 'node:crypto'

const SIGNATURE_KEY = 'B1224AC959AB2E41CF56404EBDC5677F804E9CDD88DA26E9D8DEB2B91E8466F116F66A580DC0120ABB662C672C4C76D51E360B893327D077FD6EFA95F5EA439C'

// express.raw({ type: 'application/json' }) — the signature covers the exact bytes
const want = 'sha512=' + crypto.createHmac('sha512', SIGNATURE_KEY).update(req.body).digest('hex')
const got = (req.get('X-ANET-Signature') || '').toLowerCase()
const ok = got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))

The hub-wide X-Sondahub-Webhook header on a call also gets its notifications, signed the same way (the webhook tester).

What it answers

POST/xml/v1/request.apicreateTransaction: authCaptureTransaction, authOnlyTransaction, priorAuthCaptureTransaction, captureOnlyTransaction, refundTransaction, voidTransaction — by creditCard, bankAccount, opaqueData or profile; updateHeldTransaction, sendCustomerTransactionReceipt.
POST/xml/v1/request.apiTransaction Reporting: getTransactionDetails, getUnsettledTransactionList, getSettledBatchList, getTransactionList, getBatchStatistics, getTransactionListForCustomer, getMerchantDetails, authenticateTest.
POST/xml/v1/request.apiCustomer profiles: create, get (by id, merchantCustomerId or email), ids, update, delete; payment profiles: create, get, list by expiry month, update, delete, validate; shipping addresses: create, get, update, delete; createCustomerProfileFromTransaction.
POST/xml/v1/request.apiRecurring Billing: ARBCreateSubscription, ARBGetSubscription, ARBGetSubscriptionStatus, ARBUpdateSubscription, ARBCancelSubscription, ARBGetSubscriptionList. Accept: securePaymentContainer, getHostedPaymentPage.
GET/rest/v1/webhooksWebhooks: event types, create, get, update, delete, ping; notification history.
POST/sandbox/authorizenet/payment/paymentThe hosted payment form, for a getHostedPaymentPage token.

XML or JSON in, the same out, every answer HTTP 200 with messages.resultCode Ok or Error and the real API’s codes and texts (E00003, E00007, E00027, E00039, E00040…). A request the schema refuses — out of order, an unknown element, a value too long — is answered with an ErrorResponse. Also at https://api.sondahub.com/sandbox/authorizenet/xml/v1/request.api.

Questions

Is this Authorize.net?

No — an independent imitation of Authorize.net’s API for testing, not affiliated with or endorsed by Authorize.net, Visa or Cybersource. Nothing is charged, no card network is involved and no Authorize.net account is needed.

Will the official SDKs work against it?

Yes, with the few lines above. The Python SDK (authorizenet, which posts XML and reads answers with PyXB) and the Node SDK (authorizenet, which posts JSON through axios) were both run against the sandbox, along with Authorize.net’s own sample-code runners: every sample passes except the ones for PayPal, Visa Click to Pay and split tender, which the sandbox does not model, the few that use ids from Authorize.net’s own test account, and one that expects a brand-new subscription to have payments already. The Python SDK gives up validating any answer that holds an address with two or more fields and reads it with lxml instead — its schema adds wildcards that make PyXB’s check ambiguous — and it does the same with the real API; the values read the same either way.

Why do I get E00003 when my JSON looks right?

Because the API checks elements in schema order, JSON included: transactionType before amount, amount before payment, refTransId after payment, and so on. The error names the element that is out of place and the ones it expected there, as the real one does. The SDKs build requests in order; hand-written JSON is where it bites.

Why does JSON.parse fail on the answer?

Every answer starts with a UTF-8 byte order mark, as the real API’s do. fetch’s .json() and axios drop it; Python’s requests keeps it (the header says utf-8), so r.json() raises “Unexpected UTF-8 BOM” — decode with r.content.decode('utf-8-sig'). In other languages, strip \uFEFF before parsing.

When can I refund a payment?

Once it has settled. A capture settles in the batch that closes at least 30 seconds after it, and the sandbox closes a batch every minute — so about a minute after you charge, getTransactionDetails says settledSuccessfully, the batch shows in getSettledBatchList, and a refundTransaction works. Before that, void it. The seed account’s history settles once a day, and its payment 67433922005 ($49.99, card 1111) is settled and refundable.

How are webhooks signed?

As Authorize.net signs them: X-ANET-Signature: sha512= followed by the HMAC-SHA512 of the raw body, keyed with the Signature Key, in hex. The sandbox’s Signature Key is published above; the same key makes each payment’s transHashSha2. It proves your verification code and protects nothing.

What is not modelled?

PayPal, Visa Click to Pay (Visa Checkout), Apple Pay and Google Pay payment data, split tender, track and EMV data, the hosted customer profile page (Accept Customer), mobile device and invoicing calls, Account Updater, partial authorization, and running subscriptions’ payments — subscriptions are kept and reported, but no transactions are made for them. Those calls answer E00004 (or E00013 for a payment kind) saying so. Receipts are answered Ok and no email is sent.