From Zero to a Funded Card
This recipe is the end-to-end integration milestone: a complete, runnable script that takes an empty Program from zero to a virtual card with a confirmed crypto deposit in availableBalance. It combines five guides into a single orchestrated flow and is intended as a scaffold for your own integration.
What this covers:
- Create and configure the API client (mTLS + key)
- Create a cardholder
- Submit to compliance and poll for approval
- Issue a virtual card
- Retrieve and display the card's deposit addresses
- Poll balance until a crypto deposit clears
Client setup
Every request to the Axys API requires both the mTLS client certificate and the X-API-Key header. Load both from environment variables or a secrets manager — never hardcode credentials.
// client.js — shared API client
const https = require('https');
const fs = require('fs');
const agent = new https.Agent({
cert: fs.readFileSync(process.env.AXYS_CERT_PATH),
key: fs.readFileSync(process.env.AXYS_KEY_PATH),
});
const BASE = process.env.AXYS_ENV === 'production'
? 'https://production.api.axyscards.com/v2'
: 'https://staging.api.axyscards.com/v2';
async function api(method, path, body) {
const res = await fetch(`${BASE}${path}`, {
method,
agent,
headers: {
'X-API-Key': process.env.AXYS_API_KEY,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok || json.status === 'error') {
throw Object.assign(new Error(`API error ${res.status}`), { status: res.status, body: json });
}
return json;
}
module.exports = { api };Step 1: Create the cardholder
const { api } = require('./client');
async function createCardholder() {
const result = await api('POST', '/cardholders', {
firstName: 'Jordan',
lastName: 'Reyes',
gender: 0, // 0 Male · 1 Female · 2 Other
nationality: 'American',
placeOfBirth: 'USA', // ISO 3166-1 Alpha-3
dob: '1990-05-14',
adrLine1: '123 Market Street',
city: 'San Francisco',
state: 'CA',
country: 'US', // ISO 3166-1 Alpha-2
zipCode: '94105',
emailAdr: '[email protected]',
callingCode: '001', // ITU code, zero-padded to 3 digits
cellNum: '4155551234',
countryCallingCode: 'US', // Alpha-2 country code for phoneNum
phoneNum: '4155551234',
cardHolderFirstName: 'Jordan',
cardHolderLastName: 'Reyes',
});
console.log(`✓ Cardholder created: ${result.cardholderId}`);
return result.cardholderId;
}All fields listed as
firstName,lastName,gender,nationality,placeOfBirth,dob,adrLine1,city,state,country,zipCodeare compliance-locked once the cardholder is submitted for review. Verify these before proceeding to Step 2.
Step 2: Submit to compliance and poll for approval
This is the most complex step — submission may succeed immediately or require polling over minutes or hours. The function below handles both paths with exponential backoff.
const POLL_INTERVALS_MS = [
...Array(20).fill(30_000), // Every 30 s for the first 10 min
...Array(24).fill(300_000), // Every 5 min for the next 2 hours
...Array(48).fill(1_800_000), // Every 30 min for up to 24 hours more
];
async function submitAndPollCompliance(cardholderId) {
// Submit to compliance
const { applicationLink } = await api(
'PUT', `/cardholders/${cardholderId}/kyc-submit`
);
console.log(`✓ Submitted to compliance. KYC URL: ${applicationLink}`);
console.log(' → Share this URL with the cardholder to complete identity verification.');
// Poll for outcome
for (const delayMs of POLL_INTERVALS_MS) {
await sleep(delayMs);
const cardholder = await api('GET', `/cardholders/${cardholderId}`);
switch (cardholder.status) {
case 1:
console.log(`✓ Cardholder approved (status=1)`);
return cardholder;
case 3:
console.log(` ⏳ Still under review (status=3) — continuing to poll...`);
continue;
case 5:
console.log(` ⏳ Pending (status=5) — transitional, continuing...`);
continue;
case 2:
console.error(`✗ Compliance decline (status=2).`);
console.error(` → Review with cardholder. Regenerate KYC URL or raise a support ticket.`);
throw new Error('COMPLIANCE_DECLINED');
case 6:
console.error(`✗ Error during compliance (status=6).`);
console.error(` → Attempt re-submission or raise a support ticket.`);
throw new Error('COMPLIANCE_ERROR');
case 7:
console.error(`✗ Admin decline (status=7).`);
console.error(` → Raise a support ticket — not programmatically recoverable.`);
throw new Error('ADMIN_DECLINED');
default:
console.warn(` ? Unexpected status: ${cardholder.status}`);
}
}
throw new Error('COMPLIANCE_TIMEOUT — still Under Review after 26 hours. Raise a support ticket.');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}In production, replace the in-process
sleeploop with a scheduled job (cron, queue worker, or similar) that polls on a schedule and triggers downstream steps via an event/callback whenstatus = 1is confirmed. Long-running in-process loops are fragile across deployments and restarts.
Step 3: Issue a virtual card
async function issueVirtualCard(cardholderId) {
const card = await api('POST', `/cards/virtual/${cardholderId}`, {
nameOnCard: 'JORDAN REYES',
alias: 'Primary Card',
transactionLimit: 50000, // $500.00 in a USD program
});
console.log(`✓ Virtual card issued: ${card.cardId}`);
console.log(` Masked PAN: ${card.maskedCardNumber}`);
console.log(` Deposit addresses:`);
for (const { chain, address } of card.cryptoAddresses) {
console.log(` ${chain.padEnd(8)} ${address}`);
}
return card;
}Step 4: Share the deposit address and wait for funding
const DEPOSIT_POLL_INTERVALS_MS = [
...Array(20).fill(30_000), // Every 30 s for the first 10 min
...Array(12).fill(300_000), // Every 5 min for the next hour
...Array(12).fill(600_000), // Every 10 min for the next 2 hours
];
async function waitForDeposit(cardId, expectedMinimumAmount = 1) {
console.log(`\n Waiting for deposit on card ${cardId}...`);
let pendingLogged = false;
for (const delayMs of DEPOSIT_POLL_INTERVALS_MS) {
await sleep(delayMs);
const { ledgerBalance, availableBalance, currency } = await api(
'GET', `/cards/${cardId}/balance`
);
if (ledgerBalance > 0 && !pendingLogged) {
console.log(` ⏳ Deposit pending — ledgerBalance updated to ${ledgerBalance} (currency enum: ${currency})`);
console.log(` Finality and off-ramp in progress...`);
pendingLogged = true;
}
if (availableBalance >= expectedMinimumAmount) {
console.log(`✓ Deposit complete — availableBalance: ${availableBalance} (currency enum: ${currency})`);
return { ledgerBalance, availableBalance, currency };
}
}
throw new Error(`DEPOSIT_TIMEOUT — deposit not confirmed in available balance after ~3 hours.`);
}Step 5: Assemble the full flow
async function zeroToFundedCard() {
try {
// 1. Create cardholder
const cardholderId = await createCardholder();
// 2. Compliance
await submitAndPollCompliance(cardholderId);
// 3. Issue card
const card = await issueVirtualCard(cardholderId);
// 4. Wait for deposit (in practice, your UI shows the address;
// this polls until a deposit confirms in availableBalance)
console.log(`\nShare the EVM address with the depositor: ${
card.cryptoAddresses.find(a => a.chain === 'evm')?.address
}`);
const balance = await waitForDeposit(card.cardId);
console.log(`\n✅ Complete.`);
console.log(` cardholderId: ${cardholderId}`);
console.log(` cardId: ${card.cardId}`);
console.log(` availableBalance: ${balance.availableBalance}`);
return { cardholderId, cardId: card.cardId, balance };
} catch (err) {
console.error(`\n✗ Flow aborted: ${err.message}`);
if (err.body) console.error(' API response:', JSON.stringify(err.body, null, 2));
throw err;
}
}
zeroToFundedCard();What's next
- Polling Compliance Status — production-grade backoff with queue-based polling rather than in-process sleep
- Monitoring STP Deposits — dedicated deposit-monitoring recipe
- Multi-Card Cardholder Management
