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:

  1. Create and configure the API client (mTLS + key)
  2. Create a cardholder
  3. Submit to compliance and poll for approval
  4. Issue a virtual card
  5. Retrieve and display the card's deposit addresses
  6. 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, zipCode are 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 sleep loop with a scheduled job (cron, queue worker, or similar) that polls on a schedule and triggers downstream steps via an event/callback when status = 1 is 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