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
| Scenario | How to trigger | What to verify |
|---|---|---|
| Happy path — approved | PUT /cardholders/{id}/kyc-submit → POST /staging/kyc/validate (result=pass) | status = 1; proceed to card issuance |
| Compliance decline | POST /staging/kyc/validate (result=fail) | status = 2; KYC URL refresh handled correctly (POST .../kyc-url/refresh); cardholder can re-attempt |
| Re-submission after decline | Force decline, then POST .../kyc-url/refresh → force pass | Polling resumes correctly after re-submission; status = 1 reached |
Admin decline (status = 7) | Let the emulation engine produce this organically over repeated test runs | Support ticket path triggered correctly; no automatic retry attempted |
Error during submission (status = 6) | Let the emulation engine produce this organically | Error handling path exercised; re-submission or support ticket as appropriate |
| Compliance-locked field edit attempt | Submit to compliance → attempt PUT /cardholders/{id} on a locked field | 409 returned; edit blocked as expected |
| Polling timeout | Submit to compliance, do not force outcome — let emulation run | Your polling backoff schedule (POLL_INTERVALS_MS) behaves correctly over multiple hours |
Card issuance and lifecycle
| Scenario | How to trigger | What to verify |
|---|---|---|
| Virtual card issuance | POST /cards/virtual/{cardholderId} | cardId returned; cryptoAddresses populated; issuerCardStatus = 1 immediately |
| Physical card issuance | POST /cards/assign/{cardholderId} | cardId returned; issuerCardStatus = 4 (Not Activated) |
| Physical card activation | PUT /cards/{cardId}/activate | issuerCardStatus = 1 after activation; attempt to re-activate returns error |
| Block / unblock | PUT /cards/{cardId}/status newStatus=2 → newStatus=1 | Status transitions correctly; GET /cards/{cardId}/status reflects change |
| Attempt to block a closed card | Force issuerCardStatus = 3 (via support ticket in staging) → attempt block | Returns error; state machine respected |
| Transaction limit enforcement | Issue card with low transactionLimit → POST /staging/simulate-spend with amount above limit | Spend declined or capped per limit |
| PIN change | PUT /cards/{cardId}/pin | PIN updated; incorrect oldPIN rejected |
Crypto funding (STP)
| Scenario | How to trigger | What to verify |
|---|---|---|
| Standard deposit — stablecoin | POST /staging/simulate-crypto-deposit (stablecoin tokenId) | ledgerBalance updates (pending); availableBalance updates after emulated finality |
| Deposit — slow asset | POST /staging/simulate-crypto-deposit (exotic tokenId) | Emulated longer off-ramp delay; polling logic waits correctly |
| Deposit monitoring — pending state | Poll GET /cards/{cardId}/balance after triggering | Code correctly distinguishes ledgerBalance > 0, availableBalance = 0 (pending) from complete |
| Balance after spend | Simulate deposit → simulate spend | Net balance correctly reflects both movements |
| Deposit to inactive card | Attempt simulate-crypto-deposit on card with issuerCardStatus ≠ 1 | Behavior observed — document whether deposit pends or is rejected |
Bank-rail funding
| Scenario | How to trigger | What to verify |
|---|---|---|
| Bank account registration | POST /cardholders/{id}/register-bank-account | SMS OTP confirmed in staging (emulated delivery); registration confirmed |
| Trace notification | POST /cards/{cardId}/bank-deposit | depositId returned; emulated clearing window begins |
| Clearing delay | Submit trace notification; poll GET /cards/{cardId}/balance over time | ledgerBalance updates; availableBalance updates after emulated clearing (~emulated days) |
| Missing reference / unregistered account | Submit a deposit with no senderAccountName and transactionReference mismatch | Verify suspense account path handled in your monitoring; support ticket raised |
3-D Secure / OTP
| Scenario | How to trigger | What to verify |
|---|---|---|
| End-to-end OTP delivery (webhook) | Subscribe (POST /otp/listeners) → POST /staging/prepare-spend-otp | Webhook callback received; GET /otp/{token} returns status: received and code; OTP delivered to cardholder channel |
| End-to-end OTP delivery (polling) | Subscribe without webhookUrl → POST /staging/prepare-spend-otp → poll GET /otp/{token} | status changes from pending to received; code retrieved in time |
| OTP expiry | POST /staging/prepare-spend-otp → delay retrieval beyond expiresAt | status moves to expired; expiry handling in your delivery pipeline exercised |
| Unsubscribe | DELETE /otp/{token} | GET /otp/{token} returns 404 after deletion |
Subscription by cardholderId (legacy) | Subscribe with cardholderId → POST /staging/prepare-spend-otp | Callback contains cardholderId, not cardId; routing logic handles this |
Reconciliation
| Scenario | How to trigger | What to verify |
|---|---|---|
| Transactions appear in list | POST /staging/simulate-spend → GET /cards/transactions?cardId=... | Transaction record present and correctly shaped |
| Fee records | Trigger 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 transactions | POST /wallets/transfer → GET /wallets/transactions | Transfer recorded; source wallet balance reduced, destination increased |
| Pagination handling | Create enough transactions to exceed a single page | Pagination (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/validateand 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-depositand observe the interval betweenledgerBalanceandavailableBalanceupdates. 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 pathstatus = 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
| Test | What you're checking | |
|---|---|---|
transactionLimit at exactly the ceiling | Spend at exactly the limit (via simulate-spend) — boundary accepted or rejected? | |
| Zero balance spend | simulate-spend on a card with zero availableBalance | Rejected as expected? |
| Rapid successive calls | Call the same endpoint multiple times in quick succession | No duplicate side effects; rate limiting handled correctly |
| Large pagination sets | Create 200+ cardholders; call GET /cardholders with limit=200 | Total, 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.
