Developer dashboard

Integration Guide#

Build a production-ready server integration on the Public API: take payments, handle every outcome, keep your system in sync with webhooks, and go live.


Overview#

This guide is for teams charging from their own backend — the server and ERP path. It picks up where the Quick Start ends. Using a plugin or the hosted SDK instead? Choose your integration first.

Everything here uses a server-side API key on /api/v1/public/*; you do not need a dashboard login token.


Phase 1: Set up#

1. Get Credentials#

Contact your account manager for a Sandbox account, then create API keys in the dashboard at /settings/developer/api-keys:

  • Secret API key (api-…) for server-side calls
  • SDK public key (lpk_live_…) if you collect cards in the browser with the PCI SDK
  • Production keys from https://app.auxcore.net/settings/developer/api-keys when ready

2. Choose a Client#

There is no AuxCORE server SDK package. Call the REST API directly with your HTTP client (see the Node.js, PHP and Python examples). In the browser, load the PCI SDK from https://vault.auxcore.net/sdk/auxVault.js so card data never touches your servers.

3. Configure Environment#

bash
# .env.development
AUXCORE_API_BASE=https://uat.auxcore.net/api/v1/public
AUXCORE_API_KEY=api-sandbox-key...

# .env.production
AUXCORE_API_BASE=https://app.auxcore.net/api/v1/public
AUXCORE_API_KEY=api-live-key...

Phase 2: Take a payment#

1. Initialize Client#

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

2. Process First Transaction#

Raw card numbers are rejected on Sandbox and Production. Collect the card in the browser with the SDK in tokenize only mode and post the resulting pci_tok_… token to your server. (POST /tokenize-card with a secret key also returns one, but sending card numbers from your server puts it in PCI scope — use it for Sandbox tests.)

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

3. Handle Response#

For this sale example, distinguish an approval from an accepted payment that is still pending. HTTP 200 and success: true alone do not mean the order is paid. An approval also does not confirm settlement.

javascript
const { status, body } = await processPayment(paymentData);

if (status === 200 && body.requires_action) {
  // 3DS or hosted payment page — send the payer to body.data.RedirectUrl
  // Save body.data.TransactionId; confirm the outcome via webhook or transaction lookup.
} else if (status === 200 && body.success && body.status === 'pending') {
  console.log('Payment pending:', body.data.TransactionId);
  // Save the transaction ID and mark the order as awaiting payment.
  // Wait for transaction.approved / transaction.declined; do not send a paid receipt yet.
} else if (status === 200 && body.success && body.status === 'success') {
  console.log('Transaction ID:', body.data.TransactionId);
  // Apply your approved-sale workflow, update the order and send its payment receipt once.
} else if (status === 402) {
  // Declined — body.message is safe to show; body.data.ResponseCode has the code
  console.error('Declined:', body.message);
} else {
  // Validation / auth / service error
  console.error('Error:', body.error?.message || body.message || body.error);
}

ACH can be returned after approval: subscribe to transaction.returned if you accept ACH and handle the reversal in your order and reconciliation workflows.


Phase 3: Handle errors#

Implement Comprehensive Error Handling#

Declines are HTTP 402 with a readable message and an NMI-style data.ResponseCode (e.g. 202 insufficient funds, 223 expired card, 225 invalid CVV). Other errors use { success: false, error: { code, message } }, except API-key failures, where error is a string.

javascript
function handlePaymentError(status, body) {
  if (status === 402) {
    switch (body.data?.ResponseCode) {
      case '202':
        return 'Insufficient funds. Please try a different card.';
      case '223':
      case '224':
        return 'Card expired or expiry invalid. Please check and try again.';
      case '225':
        return 'Card security code is invalid.';
      default:
        return 'Payment declined. Please try a different card.';
    }
  }
  if (status === 400) {
    // e.g. VALIDATION_ERROR, PCI_SCOPE_VIOLATION
    return body.error?.message || body.message || 'Invalid request.';
  }
  if (status === 429) return 'Too many requests. Please retry shortly.';
  if (status >= 500) return 'Payment outcome unconfirmed. Check order status before submitting again.';
  return 'Payment request failed.';
}

Recover from Timeouts#

A timeout or 5xx response can occur after the processor has accepted a charge. For POST /api/v1/public/transaction, do not automatically submit it again: this route does not forward Idempotency-Key, and OrderId is not a uniqueness constraint. Look up GET /transaction-list?orderId=…, reconcile the returned transactions with your payment attempt, and wait for its webhook or processor confirmation if the result is still unknown. An empty lookup immediately after a timeout is not proof that no charge occurred.

For endpoints that support idempotency, retry the same payment attempt with the same key and payload. See Use Idempotency Keys.


Phase 4: Webhooks#

1. Create Webhook Endpoint#

Use the validated verifySignature() helper from the Node.js webhook example. Register this raw-body route before express.json().

webhookInbox below is an adapter you implement in your application, not an AuxCORE API. Its enqueueOnce(eventId, event) must atomically store the event and durable work using a unique event ID, treat an existing ID as a successful no-op, and reject if storage fails. Return 200 only after that operation succeeds; process stored events in a worker with idempotent order updates and receipts.

javascript
// Verify against the RAW body: X-Webhook-Signature is "t=<unix>,v1=<hex>",
// v1 = HMAC-SHA256(endpointSecret, `${t}.${rawBody}`).
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'], process.env.AUXCORE_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  let event;
  try {
    event = JSON.parse(rawBody); // { id, type, created, data, metadata }
  } 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 handleRefund(event.data);
      break;

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

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

2. Register Webhook#

Register the URL with your secret key. The merchant comes from the key (ERP keys also send X-Merchant-ID). Leave out events to subscribe to the ten default transaction and session events.

bash
curl -X POST "$AUXCORE_API_BASE/register-webhook" \
  -H "x-api-key: $AUXCORE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/auxcore",
    "events": ["transaction.approved", "transaction.declined", "transaction.in_review", "transaction.refunded", "transaction.refund_failed", "transaction.returned"]
  }'

Store data.secret as AUXCORE_WEBHOOK_SECRET. Registering the same URL again updates it and rotates the secret: the old one stops verifying immediately, so deploy the new secret before relying on deliveries. See Register with an API key. You can also register and manage endpoints in the dashboard.


Phase 5: Refunds and voids#

Settled transactions are refunded; approved-but-unsettled ones should be voided (POST /void). Omit amount for a full refund.

The refund API can return HTTP 201 with success: true even when the gateway declined the refund. Read data.status: refunded records a successful refund; declined records a failure. A full refund of an approved, unsettled TSYS transaction can be routed to a void and return voided. A local void is not necessarily a confirmed gateway reversal: inspect data.gatewayResponse.gatewayVoidSuccess (or gatewayVoided and warning when calling /void).

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 submit another refund when the outcome is unknown or declined. Look up the transaction and delivery history first; declined refunds can already be queued for a server-side retry. Subscribe to transaction.refunded and transaction.refund_failed for their outcomes.


Phase 6: Test every outcome#

Run each scenario on Sandbox with the test cards and check both the API response and the webhook your server receives:

  1. Successful payment
  2. Declined payment
  3. Partial refund
  4. Full refund
  5. Void before settlement
  6. AVS mismatch and CVV mismatch, if the merchant uses those filters
  7. Webhook delivery, including a duplicate delivery of the same event.id
  8. Timeouts and 5xx, following Recover from Timeouts

Phase 7: Go live#

  1. Create production keys on https://app.auxcore.net/settings/developer/api-keys — Sandbox keys do not work there.
  2. Switch configuration:
bash
AUXCORE_API_BASE=https://app.auxcore.net/api/v1/public
AUXCORE_API_KEY=<production secret key>
AUXCORE_WEBHOOK_SECRET=<data.secret from registering your production webhook URL>
  1. Register the production webhook with the production key (Phase 4).
  2. Log every payment outcome — transaction ID, status and data.ResponseCode; never card data or keys.
  3. Run a small live payment and refund it, then watch the first real orders closely.

Work through the full Go-Live Checklist before switching traffic.


Best Practices#

  • Always use environment variables for credentials
  • Implement comprehensive error handling
  • Use webhooks for async operations
  • Test thoroughly in sandbox
  • Monitor production closely
  • Load the PCI SDK from https://vault.auxcore.net/sdk/ (never self-host it)

Next Steps#


Need help? Contact support@auxcore.net