Sandbox Testing Guide

The emulation engine exists to surface integration failures before production. This page provides a structured test approach: a test matrix covering every major flow, guidance on exercising non-happy-path and resilience scenarios, and the specific emulation behaviors that deserve the most attention.

📘

All test scenarios on this page run against the staging base URL (https://staging.api.axyscards.com/v2). Use your staging credentials throughout. Simulation endpoints (/staging/*) are staging-only — see Simulation Endpoints.


Test matrix

Cardholder onboarding

ScenarioHow to triggerWhat to verify
Happy path — approvedPUT /cardholders/{id}/kyc-submitPOST /staging/kyc/validate (result=pass)status = 1; proceed to card issuance
Compliance declinePOST /staging/kyc/validate (result=fail)status = 2; KYC URL refresh handled correctly (POST .../kyc-url/refresh); cardholder can re-attempt
Re-submission after declineForce decline, then POST .../kyc-url/refresh → force passPolling resumes correctly after re-submission; status = 1 reached
Admin decline (status = 7)Let the emulation engine produce this organically over repeated test runsSupport ticket path triggered correctly; no automatic retry attempted
Error during submission (status = 6)Let the emulation engine produce this organicallyError handling path exercised; re-submission or support ticket as appropriate
Compliance-locked field edit attemptSubmit to compliance → attempt PUT /cardholders/{id} on a locked field409 returned; edit blocked as expected
Polling timeoutSubmit to compliance, do not force outcome — let emulation runYour polling backoff schedule (POLL_INTERVALS_MS) behaves correctly over multiple hours

Card issuance and lifecycle

ScenarioHow to triggerWhat to verify
Virtual card issuancePOST /cards/virtual/{cardholderId}cardId returned; cryptoAddresses populated; issuerCardStatus = 1 immediately
Physical card issuancePOST /cards/assign/{cardholderId}cardId returned; issuerCardStatus = 4 (Not Activated)
Physical card activationPUT /cards/{cardId}/activateissuerCardStatus = 1 after activation; attempt to re-activate returns error
Block / unblockPUT /cards/{cardId}/status newStatus=2newStatus=1Status transitions correctly; GET /cards/{cardId}/status reflects change
Attempt to block a closed cardForce issuerCardStatus = 3 (via support ticket in staging) → attempt blockReturns error; state machine respected
Transaction limit enforcementIssue card with low transactionLimitPOST /staging/simulate-spend with amount above limitSpend declined or capped per limit
PIN changePUT /cards/{cardId}/pinPIN updated; incorrect oldPIN rejected

Crypto funding (STP)

ScenarioHow to triggerWhat to verify
Standard deposit — stablecoinPOST /staging/simulate-crypto-deposit (stablecoin tokenId)ledgerBalance updates (pending); availableBalance updates after emulated finality
Deposit — slow assetPOST /staging/simulate-crypto-deposit (exotic tokenId)Emulated longer off-ramp delay; polling logic waits correctly
Deposit monitoring — pending statePoll GET /cards/{cardId}/balance after triggeringCode correctly distinguishes ledgerBalance > 0, availableBalance = 0 (pending) from complete
Balance after spendSimulate deposit → simulate spendNet balance correctly reflects both movements
Deposit to inactive cardAttempt simulate-crypto-deposit on card with issuerCardStatus ≠ 1Behavior observed — document whether deposit pends or is rejected

Bank-rail funding

ScenarioHow to triggerWhat to verify
Bank account registrationPOST /cardholders/{id}/register-bank-accountSMS OTP confirmed in staging (emulated delivery); registration confirmed
Trace notificationPOST /cards/{cardId}/bank-depositdepositId returned; emulated clearing window begins
Clearing delaySubmit trace notification; poll GET /cards/{cardId}/balance over timeledgerBalance updates; availableBalance updates after emulated clearing (~emulated days)
Missing reference / unregistered accountSubmit a deposit with no senderAccountName and transactionReference mismatchVerify suspense account path handled in your monitoring; support ticket raised

3-D Secure / OTP

ScenarioHow to triggerWhat to verify
End-to-end OTP delivery (webhook)Subscribe (POST /otp/listeners) → POST /staging/prepare-spend-otpWebhook callback received; GET /otp/{token} returns status: received and code; OTP delivered to cardholder channel
End-to-end OTP delivery (polling)Subscribe without webhookUrlPOST /staging/prepare-spend-otp → poll GET /otp/{token}status changes from pending to received; code retrieved in time
OTP expiryPOST /staging/prepare-spend-otp → delay retrieval beyond expiresAtstatus moves to expired; expiry handling in your delivery pipeline exercised
UnsubscribeDELETE /otp/{token}GET /otp/{token} returns 404 after deletion
Subscription by cardholderId (legacy)Subscribe with cardholderIdPOST /staging/prepare-spend-otpCallback contains cardholderId, not cardId; routing logic handles this

Reconciliation

ScenarioHow to triggerWhat to verify
Transactions appear in listPOST /staging/simulate-spendGET /cards/transactions?cardId=...Transaction record present and correctly shaped
Fee recordsTrigger simulation that would incur a fee (e.g., cross-currency spend)Fee record appears in GET /cards/fees; authRefNum links to transaction
Wallet transfer in wallet transactionsPOST /wallets/transferGET /wallets/transactionsTransfer recorded; source wallet balance reduced, destination increased
Pagination handlingCreate enough transactions to exceed a single pagePagination (total, limit, offset) handled correctly in your reconciliation job

Resilience and non-happy-path testing

The emulation engine is specifically designed to surface these scenarios organically over repeated test runs. Complement the deterministic simulation endpoints (above) with unforced emulation runs where you don't call any /staging/* endpoint and observe what the engine produces naturally.

Timing and asynchronicity

Test your polling logic against the emulation engine's natural timing distributions — don't assume the happy-path test sequence (force a KYC pass, get an instant result) is representative of production behavior. Specifically:

  • Run your full onboarding flow without calling POST /staging/kyc/validate and observe how long the emulation engine takes to produce an outcome. Your polling backoff schedule should handle both fast (seconds) and slow (hours) reviews without timing out prematurely or hammering the API.
  • Run POST /staging/simulate-crypto-deposit and observe the interval between ledgerBalance and availableBalance updates. This emulates the chain-specific confirmation and off-ramp timing — your monitoring code should not assume they arrive close together.

Error injection

The emulation engine produces some error conditions organically:

  • status = 6 (Error during KYC submission) — observe your error handling and re-submission path
  • status = 7 (Admin Decline) — observe your support-ticket escalation path
  • Webhook delivery delays — observe whether your OTP delivery pipeline has adequate timeout handling
  • transStatus = 4 (Declined transaction) — the emulation engine can produce declined authorizations; observe how your reconciliation code handles these

Boundary conditions

TestWhat you're checking
transactionLimit at exactly the ceilingSpend at exactly the limit (via simulate-spend) — boundary accepted or rejected?
Zero balance spendsimulate-spend on a card with zero availableBalanceRejected as expected?
Rapid successive callsCall the same endpoint multiple times in quick successionNo duplicate side effects; rate limiting handled correctly
Large pagination setsCreate 200+ cardholders; call GET /cardholders with limit=200Total, offset, and final-page detection correct

Building an automated test suite

The simulation endpoints are designed to be called programmatically — making it straightforward to build an automated integration test suite that runs the full test matrix on every deployment. A suggested structure:

// test-suite.js — runs against staging only
const assert = process.env.AXYS_ENV === 'staging'
  || (() => { throw new Error('Tests must run against staging') })();

describe('Cardholder onboarding', () => {
  it('reaches Approved after KYC pass', async () => { /* ... */ });
  it('reaches Compliance Decline after KYC fail', async () => { /* ... */ });
  it('handles re-submission correctly', async () => { /* ... */ });
});

describe('Card funding — crypto STP', () => {
  it('updates ledgerBalance before availableBalance', async () => { /* ... */ });
  it('polling logic handles emulated delay correctly', async () => { /* ... */ });
});

describe('3DS OTP', () => {
  it('delivers OTP before expiresAt', async () => { /* ... */ });
  it('handles OTP expiry gracefully', async () => { /* ... */ });
});

Guard every test suite with an environment check — process.env.AXYS_ENV !== 'staging' should prevent the suite from running against the production API key, even if one is accidentally in scope.


What's next