Developer dashboard

Node.js Examples#

Complete Node.js/JavaScript integration examples.

Note: AuxCORE does not publish a Node.js SDK package. These examples call the REST API directly with fetch (Node.js 18+). In the browser, collect cards with the PCI SDK (https://vault.auxcore.net/sdk/auxVault.js) instead.


Setup#

javascript
const BASE_URL = process.env.AUXCORE_API_BASE || 'https://uat.auxcore.net/api/v1/public';
const API_KEY = process.env.AUXCORE_API_KEY;

async function auxcore(method, path, body) {
  const res = await fetch(`${BASE_URL}${path}`, {
    method,
    headers: {
      'x-api-key': API_KEY,
      'Content-Type': 'application/json',
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json().catch(() => ({}));
  return { status: res.status, body: json };
}

Tenant-wide ERP keys (lv_erp_…) must also send X-Merchant-ID: <merchant uuid>.


Process Payment#

Raw card numbers are rejected on Sandbox and Production, so charge a pci_tok_… token from the PCI SDK or from POST /tokenize-card (server-side key only).

For this sale example, an accepted pending payment is awaiting its outcome. Save its transaction ID and wait for transaction.approved / transaction.declined before marking an order paid or sending a paid receipt. status: 'success' means approved, not necessarily settled. Handle transaction.returned as well if you accept ACH.

javascript
async function processPayment({ pciToken, amount, name, email, orderId }) {
  const { status, body } = await auxcore('POST', '/transaction', {
    Amount: amount,
    pciToken,
    BillingCustomerName: name,
    BillingEmail: email,
    OrderId: orderId,
  });

  // 200: approved (status "success") or accepted-pending (status "pending", e.g. ACH)
  // 200 + requires_action: redirect the payer to body.data.RedirectUrl (3DS / hosted page)
  // 402: declined; 400/401/5xx: request or service error
  if (status === 200 && body.requires_action) {
    return {
      success: false,
      status: 'pending',
      requiresAction: true,
      redirectUrl: body.data.RedirectUrl,
      transactionId: body.data.TransactionId,
    };
  }
  if (status === 200 && body.success && body.status === 'pending') {
    console.log('Payment pending:', body.data.TransactionId);
    return { success: true, status: 'pending', transaction: body.data };
  }
  if (status === 200 && body.success && body.status === 'success') {
    console.log('Transaction approved:', body.data.TransactionId, body.data.AuthCode);
    return { success: true, status: 'approved', transaction: body.data };
  }
  console.error('Payment failed:', body.message || body.error);
  return {
    success: false,
    status: body.status || 'error',
    error: body.message || body.error,
    responseCode: body.data?.ResponseCode,
  };
}

Process Refund#

Omit amount for a full refund. transactionId accepts the TransactionId UUID or the TXN-… reference.

A gateway-declined refund can still return HTTP 201 with success: true; read the refund row's data.status. A full refund of an approved, unsettled TSYS transaction can instead return a voided transaction. Inspect data.gatewayResponse.gatewayVoidSuccess (or gatewayVoided and warning on /void) before reporting a local void as a confirmed gateway reversal.

javascript
async function processRefund(transactionId, amount) {
  const { status, body } = await auxcore('POST', '/refund', { transactionId, amount });
  const outcome = body.data?.status;
  if (status >= 200 && status < 300 && body.success) {
    if (outcome === 'refunded') {
      return { success: true, outcome, refund: body.data };
    }
    if (outcome === 'voided') {
      return {
        success: true,
        outcome,
        transaction: body.data,
        gatewayVoided: body.gatewayVoided ?? (body.data?.gatewayResponse?.gatewayVoidSuccess === true),
        warning: body.warning,
      };
    }
  }
  return {
    success: false,
    outcome: outcome || 'unknown',
    refund: body.data,
    error: body.error?.message || body.message || body.data?.responseText || 'Refund not confirmed',
  };
}

Do not automatically resubmit an unknown or declined refund. Check the transaction and delivery history; declined refunds can already be queued for a server-side retry. Subscribe to transaction.refunded and transaction.refund_failed.


Create Subscription#

Recurring plans are created with POST /recurring (forwarded to the recurring service; merchantId comes from the key).

javascript
async function createSubscription({ customerId, name, email, pciToken }) {
  const { status, body } = await auxcore('POST', '/recurring', {
    customerId,
    recurringUserName: name,
    customerEmail: email,
    recurringAmount: 99.00,
    period: 'Monthly',
    recurringDate1: '2026-11-01',
    maximumOccurrence: 'NoExpire',
    suggestedMode: 'CARD',
    children: [{ pciToken, isDefault: true }],
  });
  if (status === 201) {
    console.log('Subscription created:', body.data.id);
    return { success: true, subscription: body.data };
  }
  return { success: false, error: body.error?.message };
}

Webhook Handler (Express)#

Webhooks are signed with the endpoint secret returned when you created it. The X-Webhook-Signature header is t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>". Verify against the raw body.

Copy verifySignature() below into a shared server helper and reuse it in your integration. Missing or malformed headers return false. Register the raw-body route before express.json().

webhookInbox is an adapter you implement, not an AuxCORE SDK. enqueueOnce(eventId, event) must atomically save the event and durable work using a unique event ID, succeed without enqueueing again for duplicates, and reject on storage failure. A worker calls handleQueuedEvent() for stored events and uses idempotent business updates and receipts so worker retries are safe.

javascript
const express = require('express');
const crypto = require('crypto');
const app = express();

const WEBHOOK_SECRET = process.env.AUXCORE_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET) throw new Error('AUXCORE_WEBHOOK_SECRET is required');

function verifySignature(rawBody, header, secret, toleranceSec = 300) {
  if (typeof rawBody !== 'string' || typeof header !== 'string' ||
      typeof secret !== 'string' || !secret) return false;
  if (!Number.isFinite(toleranceSec) || toleranceSec < 0) return false;

  // Matches the sender's t=<unix>,v1=<64 hex characters> header format.
  const match = /^t=(\d+),v1=([a-fA-F0-9]{64})$/.exec(header);
  if (!match) return false;
  const timestamp = Number(match[1]);
  if (!Number.isSafeInteger(timestamp) || timestamp <= 0 ||
      Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSec) return false;

  const expected = crypto.createHmac('sha256', secret)
    .update(`${match[1]}.${rawBody}`, 'utf8').digest();
  const received = Buffer.from(match[2], 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

app.post('/webhooks/auxcore', express.raw({ type: 'application/json' }), async (req, res, next) => {
  if (!Buffer.isBuffer(req.body)) return res.status(400).send('Expected raw JSON body');
  const rawBody = req.body.toString('utf8');
  if (!verifySignature(rawBody, req.headers['x-webhook-signature'], WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  let event;
  try {
    event = JSON.parse(rawBody); // { id, type, created, data, metadata: { tenantId, merchantId } }
  } catch {
    return res.status(400).send('Invalid JSON');
  }
  if (!event || typeof event.id !== 'string' || !event.id) {
    return res.status(400).send('Missing event ID');
  }

  try {
    await webhookInbox.enqueueOnce(event.id, event);
    return res.sendStatus(200);
  } catch (error) {
    return next(error); // Storage failed: return 5xx so delivery can be retried.
  }
});

// Called by your worker for events saved in the inbox.
async function handleQueuedEvent(event) {
  switch (event.type) {
    case 'transaction.approved':
      await handleTransactionApproved(event.data);
      break;
    case 'transaction.declined':
      await handleTransactionDeclined(event.data);
      break;
    case 'transaction.refunded':
      await handleTransactionRefunded(event.data);
      break;

    case 'transaction.refund_failed':
      await handleRefundFailed(event.data);
      break;

    case 'transaction.returned':
      await handleAchReturn(event.data);
      break;
  }
}

async function handleTransactionApproved(data) {
  console.log('Transaction approved:', data.transactionId, data.amount);
  // Update order status (data.orderId), send receipt, etc.
}

async function handleTransactionDeclined(data) {
  console.log('Transaction declined:', data.transactionId, data.responseText);
}

async function handleTransactionRefunded(data) {
  console.log('Refund recorded:', data.transactionId, 'for', data.originalTransactionId);
}

async function handleRefundFailed(data) {
  console.log('Refund failed:', data.transactionId, data.responseText);
  // Keep the refund unresolved; check its retry queue before submitting another refund.
}

async function handleAchReturn(data) {
  console.log('ACH returned:', data.transactionId, data.returnCode);
  // Reverse your paid-order state and reconcile the returned debit.
}

app.listen(3000, () => {
  console.log('Webhook server running on port 3000');
});

Error Handling#

Public API errors come in two shapes: API-key failures are 401 with a string error ({ success: false, error: "API key validation failed", message } for an unknown or deactivated key, "API key required" when none is sent), other errors use { success: false, error: { code, message } }. Declines are HTTP 402 with data.ResponseCode and a readable message.

javascript
async function processPaymentWithErrorHandling(payment) {
  const { status, body } = await auxcore('POST', '/transaction', payment);

  if (status === 200) {
    return {
      success: body.success,
      status: body.status,
      requiresAction: body.requires_action === true,
      redirectUrl: body.data?.RedirectUrl,
      transaction: body.data,
    };
  }

  switch (status) {
    case 402:
      return { success: false, message: body.message || 'Payment declined. Please try a different card.' };
    case 400:
      return { success: false, message: body.error?.message || body.message || 'Invalid request.' };
    case 401:
    case 403:
      return { success: false, message: 'Check your API key.' };
    case 429:
      return { success: false, retryable: true, message: 'Rate limited — retry later.' };
    default:
      return {
        success: false,
        retryable: false,
        outcomeUnknown: status >= 500,
        message: 'Payment outcome unconfirmed. Check the transaction before submitting again.',
      };
  }
}

Retry with Exponential Backoff#

The loop below is for read requests, such as the transaction lookup shown here. Retry reads on 429 and 5xx with backoff. For writes protected by idempotency, reuse the same persisted key and exact payload; see Use Idempotency Keys.

Do not automatically retry a public /transaction after a timeout or 5xx: this route does not forward Idempotency-Key, and OrderId does not deduplicate charges. Look up GET /transaction-list?orderId=… and wait for the webhook or confirm with the processor if the outcome remains unknown. An empty immediate lookup does not establish that the payment failed. Check the existing result before resubmitting voids or refunds as well.

javascript
async function retryWithBackoff(fn, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    const result = await fn();
    if (!result.retryable || attempt === maxRetries) return result;

    const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
    console.log(`Retry attempt ${attempt} after ${delay}ms`);
    await sleep(delay);
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Usage
const result = await retryWithBackoff(() =>
  auxcore('GET', '/transaction-list?orderId=ORD-1001').then(({ status, body }) => ({
    retryable: status === 429 || status >= 500,
    ...body,
  }))
);

Next: PHP examples →