# 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](../getting-started/get-started.md#charge-from-your-server-or-erp). It picks up where the [Quick Start](../getting-started/quickstart.md) ends. Using a plugin or the hosted SDK instead? [Choose your integration](../getting-started/get-started.md) 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](../examples/nodejs.md), [PHP](../examples/php.md) and [Python](../examples/python.md) 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](../getting-started/sdks.md#tokenize-only-charge-from-your-server) 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](./best-practices.md#use-idempotency-keys).

---

## Phase 4: Webhooks

### 1. Create Webhook Endpoint

Use the validated `verifySignature()` helper from the [Node.js webhook example](../examples/nodejs.md#webhook-handler-express). 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](../getting-started/webhooks.md#register-with-an-api-key-public-api). 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](../getting-started/testing.md#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](#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>
```

3. **Register the production webhook** with the production key (Phase 4).
4. **Log every payment outcome** — transaction ID, `status` and `data.ResponseCode`; never card data or keys.
5. **Run a small live payment and refund it**, then watch the first real orders closely.

Work through the full [Go-Live Checklist](./go-live-checklist.md) 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

- [Go-live checklist →](./go-live-checklist.md)
- [Best practices →](./best-practices.md)
- [Troubleshooting →](./troubleshooting.md)

---

**Need help?** Contact support@auxcore.net
