# Transactions API

Process credit card, ACH, cash, and tokenized payments with the AuxCORE Transactions API.

---

## Overview

The Transactions API allows you to:
- **Process sales** — One-step payment authorization and capture
- **Authorize payments** — Reserve funds for later capture
- **Capture authorized payments** — Complete a previously authorized transaction
- **Void transactions** — Cancel a transaction before settlement
- **Refund payments** — Return funds to customers (full or partial)
- **Issue credits** — Record a credit against a sale, or push an unlinked credit to a card
- **Record cash payments** — Log cash transactions with immediate settled status
- **Record external SDK payments** — Log IPOS/NearPay device payments with the full platform fee breakdown

---

## API Surfaces

AuxCORE has three paths for processing transactions. Choose based on your integration type:

| Surface | Base Path | Auth | Use Case |
|---------|-----------|------|----------|
| **Public API** | `/api/v1/public/` | `x-api-key` (tenant + merchant come from the key; tenant/ERP keys also send `X-Merchant-ID`) | ERP, server-to-server, plugins |
| **Admin API** | `/api/v1/transactions/` | `Authorization: Bearer JWT` + `X-Tenant-ID` | Dashboard integrations |
| **AuxCORE SDK API (v2.0)** | `/api/v1/auxvault/` | Server-side `x-api-key`; tenant/ERP keys also need `X-Merchant-ID`; SDK public keys get `403` | Official AuxCORE SDK namespace |

**For ERP and server integrations, use the Public API.** The current `auxVault.js` also uses `/api/v1/public/transaction` with `pciToken`. The current `/auxvault/transaction` adapter drops PCI tokens and always calls sale, even with `TransactionType: "0"`; use Public API for auth-only. `/api/v1/auxvault` itself has no root handler. See [AuxCORE SDK contracts](../reference/endpoints.md#auxcore-sdk-api).

---

## Transaction Types

| Type | Description | Use Case |
|------|-------------|----------|
| `sale` | Auth + Capture in one step | Immediate payment |
| `auth` | Reserve funds only | Hotels, rentals, pre-orders |
| `capture` | Capture a previously authorized transaction | After service delivered |
| `void` | Cancel before settlement (the transaction's `status` becomes `voided`) | Order cancelled |
| `refund` | Return funds (full or partial) — recorded as a new `refund` row | Returns, disputes |
| `credit` | Credit not tied to a captured sale | Store credit, compensation |

## Transaction Statuses

The `status` field on a transaction record is one of: `pending`, `approved`, `declined`, `settled`, `voided`, `refunded`, `error`, `reversed` (ACH return), `processing` (ACH debit sent to the bank).

## Payment Methods

| Method | Description | Transaction ID Prefix |
|--------|-------------|----------------------|
| `Card` (default) | Credit/debit card via gateway | `TXN-` |
| `ACH` | Bank account transfer | `ACH-` |
| `Cash` | Physical cash — no gateway, settled immediately | `CASH-` |

Refund rows use the prefix `REF-` and credit rows `CREDIT-`. Every transaction also has a UUID `id`; endpoints that take a transaction ID accept either the UUID or the prefixed `transactionId`.

---

## Process a Sale — Public API

### Endpoint

```http
POST /api/v1/public/transaction
```

### Required Headers

```http
x-api-key: YOUR_API_KEY
Content-Type: application/json
```

Tenant and merchant are resolved from a merchant-scoped API key. With a tenant-scoped / ERP key (`lv_erp_…`), also send `X-Merchant-ID: <merchantId>` (or `merchantId` in the body).

> **PCI:** On Sandbox and Production, raw card data (`CardNumber` / `CVV`) is rejected with `400 PCI_SCOPE_VIOLATION`. Collect the card with the PCI SDK (`https://vault.auxcore.net/sdk/`) and send the returned `pciToken` instead. Every card example on this page uses a `pciToken`.

### Card Payment (PCI token)

```bash
curl -X POST https://uat.auxcore.net/api/v1/public/transaction \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "Amount": 150.75,
    "SuggestedMode": "Card",
    "pciToken": "pci_tok_8f2c1a9b7e4d",
    "BillingCustomerName": "John Doe",
    "BillingEmail": "john.doe@example.com",
    "BillingAddress": "123 Main Street",
    "BillingCity": "New York",
    "BillingState": "NY",
    "BillingPostalCode": "10001",
    "OrderId": "ORDER-2026-001",
    "Description": "Widget purchase"
  }'
```

### Auth-Only Payment (Reserve Funds)

Use `transactionType: "auth"` (legacy numeric value `"2"` is also accepted) to reserve funds without capturing. Follow up with a capture (Admin API or AuxCORE SDK capture route — see Captures) once the service or goods are delivered.

```bash
curl -X POST https://uat.auxcore.net/api/v1/public/transaction \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "Amount": 150.75,
    "transactionType": "auth",
    "SuggestedMode": "Card",
    "pciToken": "pci_tok_8f2c1a9b7e4d",
    "BillingCustomerName": "John Doe",
    "BillingEmail": "john.doe@example.com",
    "BillingPostalCode": "10001",
    "OrderId": "ORDER-2026-001"
  }'
```

A successful authorization is stored with `type: "auth"` and `status: "approved"`, so the Public API response carries `Status: "1"` — the same as an approved sale.

### ACH Payment

```bash
curl -X POST https://uat.auxcore.net/api/v1/public/transaction \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "Amount": 500.00,
    "SuggestedMode": "ACH",
    "account_number": "1234567890",
    "routing_number": "021000021",
    "account_type": "checking",
    "account_holder_name": "John Doe",
    "BillingCustomerName": "John Doe",
    "BillingEmail": "john.doe@example.com"
  }'
```

ACH is asynchronous: an accepted debit comes back with HTTP `200`, `status: "pending"`, `Status: "2"` and `Settled: false`. Watch the `transaction.approved` / `transaction.declined` webhooks (or poll the transaction) for the outcome; a later bank return arrives as `transaction.returned`.

### Cash Payment

Send `"SuggestedMode": "Cash"` with an `Amount` to record a cash sale. No gateway is called; the transaction is stored as `settled` with no convenience fee (see Cash Payment below for the rules).

### Request Fields

#### Core Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `Amount` | number | **Yes** | Transaction amount in USD (e.g., `125.00`). Must be > 0 |
| `transactionType` | string | No | `"sale"` (default) or `"auth"` (`TransactionType` and the numeric `"2"` for auth are also accepted) |
| `SuggestedMode` | string | No | `"Card"` (default), `"ACH"` or `"Cash"` |
| `BaseAmount` | number | No | Amount before tip, tax and fee |
| `TipAmount` | number | No | Tip amount. Default `0` |
| `TaxAmount` | number | No | Tax amount. Default `0` |
| `ConvenienceFeeActive` | boolean | No | Who pays the technology fee. Omit for the merchant's default payer; `true` — the customer pays it; `false` — the merchant absorbs it. Honoured when the merchant's account allows changing who pays; otherwise it is ignored, the merchant's pricing decides, and the response carries the warning `payer_change_not_allowed` |
| `Currency` | string | No | ISO 4217 code. Default `"USD"` |
| `OrderId` | string | No | Your order reference number |
| `InvoiceNumber` | string | No | Invoice reference |
| `Description` | string | No | Transaction description |
| `BranchId` | string (uuid) | No | Location (branch) the sale belongs to |
| `saveCard` | boolean | No | With a `pciToken`, save the card to the customer vault after approval (needs `BillingEmail` or `BillingPhoneNumber`) |
| `ReturnUrl` / `DeclineUrl` / `CancelUrl` | string | No | Where to send the payer after a redirect (3DS / hosted page). When supplied, the response includes `ReturnLandingUrl` |
| `metadata` | object | No | Free-form key/value data |

> **Default behavior:** If `transactionType` is omitted, the transaction runs as a `sale` (authorize + capture in one step).

#### Card Fields (when `SuggestedMode = "Card"`)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `pciToken` | string | Yes* | PCI vault token (`pci_tok_…`) from the PCI SDK or a saved card |
| `CardNumber` | string | No | Not accepted: raw card numbers are rejected on Sandbox and Production with `400 PCI_SCOPE_VIOLATION`. Send `pciToken` |
| `ExpiryMonth` | string | Dev only | 2-digit month (`"01"`–`"12"`) |
| `ExpiryYear` | string | Dev only | 4-digit year (e.g., `"2027"`) |
| `CVV` | string | Dev only | 3-4 digit security code |
| `cardLast4` / `cardBrand` | string | No | Display metadata returned by the PCI iframe |

*`token`, `paymentToken` or `cardToken` holding a `pci_tok_…` value are accepted as aliases.

#### ACH Fields (when `SuggestedMode = "ACH"`)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `account_number` | string | Yes | Bank account number (`AccountNumber` also accepted) |
| `routing_number` | string | Yes | Bank routing number (`RoutingNumber` also accepted) |
| `account_type` | string | No | `"checking"` (default) or `"savings"` |
| `account_holder_name` | string | No | Name on the account. Falls back to `BillingCustomerName` |
| `SecCode` | string | No | ACH SEC code. Inferred (`WEB`) when absent |
| `CustomerType` | string | No | Account holder type (e.g. business) |
| `BusinessName` | string | No | Company name for a business bank account |

#### Billing Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `BillingCustomerName` | string | Recommended | Cardholder name (or `FirstName` + `LastName`) |
| `BillingEmail` | string | Recommended | Customer email (for receipts) |
| `BillingPhoneNumber` | string | No | Customer phone |
| `BillingAddress` | string | Conditional* | Street address |
| `BillingCity` | string | Conditional* | City |
| `BillingState` | string | Conditional* | 2-letter state code |
| `BillingPostalCode` | string | Conditional* | ZIP/postal code |
| `BillingCountry` | string | No | Country |

*Required if AVS filtering is enabled for this merchant

### Response

HTTP `200` for an approved or accepted (pending ACH) transaction, `402` for a decline.

```json
{
  "success": true,
  "status": "success",
  "status_code": 200,
  "message": "Approved",
  "data": {
    "TransactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "Status": "1",
    "Amount": 155.27,
    "BaseAmount": 150.75,
    "ConvenienceFee": 4.52,
    "TipAmount": 0,
    "TaxAmount": 0,
    "ConvenienceFeeActive": true,
    "CardNumber": "****1111",
    "BillingEmail": "john.doe@example.com",
    "BillingCustomerName": "John Doe",
    "ResponseCode": "100",
    "ResponseMessage": "Approved",
    "AuthCode": "123456",
    "AvsResult": "Y",
    "CvvResult": "M",
    "GatewayTransactionId": "3456789012",
    "ProcessedAt": "2026-02-23T14:00:00.000Z",
    "otpSent": false,
    "watchdog": {}
  }
}
```

- `TransactionId` is the transaction's UUID `id`.
- `status` is `"success"`, `"pending"` (accepted, not settled — ACH) or `"declined"`. Read `status`, not just `success`.
- `Amount` is the fee-inclusive total; `ConvenienceFee`, `TipAmount` and `TaxAmount` are always present (`0` when not applied).
- `warnings` (top level) appears only when the fee was not applied as requested: a convenience fee was requested but none was applied, or `ConvenienceFeeActive` was ignored because the merchant's account does not allow changing who pays (`payer_change_not_allowed`).
- `customerId` / `paymentMethodId` are added when `saveCard` saved the card.
- `VelocityReviewStatus: "in_review"` is added when one of the merchant's Transaction Rules held an approved payment for review. `Status` stays `"1"`, but the payment is not complete: do not fulfil until `transaction.approved` arrives. `transaction.voided` means the merchant denied it (see [Webhook Events](../reference/webhook-events.md)).

### Status Codes

| Status | Meaning |
|--------|---------|
| `"1"` | Approved (sale or auth) |
| `"2"` | Pending (accepted ACH, or awaiting a customer redirect) |
| `"3"` | Processing |
| `"9"` | Declined / error |

### Redirect Required (3DS / hosted payment page)

Some gateways need the payer to complete an action first. The response is HTTP `200` with `success: false`, `status: "pending"` and `requires_action: true`:

```json
{
  "success": false,
  "status": "pending",
  "status_code": 200,
  "requires_action": true,
  "message": "Cardholder authentication required (3DS)",
  "data": {
    "TransactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "Status": "2",
    "RequiresAction": true,
    "RedirectUrl": "https://acs.example-bank.com/challenge/abc123",
    "RedirectType": "three_ds",
    "PaymentMode": "card",
    "Url3Ds": "https://acs.example-bank.com/challenge/abc123",
    "Amount": 150.75
  }
}
```

Send the payer to `RedirectUrl`. `RedirectType` is `three_ds`, `hosted_checkout` or `hosted_payment_page`. `Url3Ds` is deprecated (same value). `ReturnLandingUrl` is included when you sent `ReturnUrl` / `DeclineUrl` / `CancelUrl`.

---

## Void a Transaction — Public API

Cancel an approved transaction before it settles.

### Endpoint

```http
POST /api/v1/public/void
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/public/void \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
  }'
```

| Field | Required | Description |
|-------|----------|-------------|
| `transactionId` | **Yes** | Transaction UUID or `TXN-…` ID (`TransactionId` / `transaction_id` also accepted) |

The response is the voided transaction: `{ "success": true, "data": { ...transaction, "status": "voided" }, "gatewayVoided": true }`. A failed void returns `400` with `error.code: "VALIDATION_ERROR"` and the reason in `error.message`.

> **Note:** Only `approved` transactions (and `pending` ACH debits) can be voided. Once a transaction is `settled`, use `/refund` instead. Cash transactions cannot be voided.

---

## Refund a Transaction — Public API

Issue a full or partial refund for an approved or settled transaction.

### Endpoint

```http
POST /api/v1/public/refund
```

```bash
# Full refund
curl -X POST https://uat.auxcore.net/api/v1/public/refund \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
  }'

# Partial refund
curl -X POST https://uat.auxcore.net/api/v1/public/refund \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "amount": 50.00
  }'
```

| Field | Required | Description |
|-------|----------|-------------|
| `transactionId` | **Yes** | Transaction UUID or `TXN-…` ID |
| `amount` | No | Partial refund amount (> 0). Omit for full refund. |

Returns HTTP `201` with the new refund transaction (`REF-…`) in `data`. **Check `data.status`:** `refunded` means the gateway accepted it, `declined` means it was refused (a gateway decline is added to the Refund Decline Queue) — `success` is `true` in both cases. Only a lowercase `amount` makes it partial; a PascalCase `Amount` is ignored and the full amount is refunded.

---

## Get Transaction — Public API

Retrieve the full details of a specific transaction.

### Endpoint

```http
GET /api/v1/public/transaction/:id
```

```bash
curl https://uat.auxcore.net/api/v1/public/transaction/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
  -H "x-api-key: YOUR_API_KEY"
```

`:id` is the UUID or the `TXN-…` / `ACH-…` / `CASH-…` ID. The response is `{ "success": true, "data": { ...transaction } }` with the stored transaction fields (camelCase: `transactionId`, `status`, `type`, `amount`, `baseAmount`, `convenienceFee`, `cardLast4`, …) plus `refundedAmount` and, when available, `merchantName`. Returns `404 NOT_FOUND` for a transaction outside the key's merchant.

---

## List Transactions — Public API

Search and filter transactions for the API key's merchant.

### Endpoint

```http
GET /api/v1/public/transaction-list
```

`GET /api/v1/public/merchant-transactions` is an alias with identical behaviour.

```bash
# All recent transactions
curl "https://uat.auxcore.net/api/v1/public/transaction-list" \
  -H "x-api-key: YOUR_API_KEY"

# Filter by date range
curl "https://uat.auxcore.net/api/v1/public/transaction-list?dateFrom=2026-02-01&dateTo=2026-02-28" \
  -H "x-api-key: YOUR_API_KEY"

# Filter by status
curl "https://uat.auxcore.net/api/v1/public/transaction-list?status=approved" \
  -H "x-api-key: YOUR_API_KEY"
```

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `page` | integer | Page number (default: 1) |
| `limit` | integer | Items per page (default: 50) |
| `dateFrom` | date | Filter start date (ISO 8601). A plain `YYYY-MM-DD` `dateTo` covers the whole day |
| `dateTo` | date | Filter end date (ISO 8601) |
| `dateField` | string | `settledAt` to filter on settlement date instead of creation date |
| `status` | string | `pending`, `approved`, `declined`, `settled`, `voided`, `refunded`, `error`, `reversed`, `processing` (comma-separated for several) |
| `excludeStatus` | string | Statuses to leave out (comma-separated) |
| `type` | string | `sale`, `auth`, `capture`, `refund`, `credit` (`void` matches voided transactions) |
| `paymentMethod` | string | `card`, `ach`, `cash`, `crypto`, `gift` |
| `q` | string | Free-text search across name, transaction ID, order ID, amount, email, card last 4, auth code |
| `transactionId` / `orderId` / `email` / `cardLast4` / `name` / `amount` | string | Field filters (ignored when `q` is sent) |
| `customerId` | string (uuid) | Transactions for one customer |
| `sort` | string | `asc` or `desc` (default) |
| `sortColumn` | string | `createdAt` (default), `amount`, `status`, `type`, `transactionId`, `orderId` |

### Response

```json
{
  "success": true,
  "data": [ { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "transactionId": "TXN-1771855200000-A1B2C3D4", "status": "approved", "type": "sale", "amount": "150.75" } ],
  "meta": {
    "page": 1,
    "limit": 50,
    "total": 1,
    "totalPages": 1,
    "summary": { "count": 1, "amountTotal": 150.75, "approvedCount": 1, "refundTotal": 0 },
    "statusCounts": { "approved": 1 }
  }
}
```

---

## Token-Based Transaction — Public API

Charge a saved card by its PCI vault token. The body is passed to the transaction service as-is (camelCase fields), and the merchant comes from the API key.

### Endpoint

```http
POST /api/v1/public/token-transaction
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/public/token-transaction \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "pciToken": "pci_tok_8f2c1a9b7e4d",
    "amount": 125.00,
    "description": "Monthly renewal",
    "billingEmail": "john.doe@example.com"
  }'
```

| Field | Required | Description |
|-------|----------|-------------|
| `pciToken` | **Yes** | Saved PCI vault token (`pci_tok_…`); `cardToken` / `paymentToken` holding a `pci_tok_…` value also work |
| `amount` | **Yes** | Amount (> 0) |
| `transactionType` | No | `sale` (default) |
| `description`, `orderId`, `billingEmail`, `billingCustomerName` | No | Same meaning as on the Admin API sale |

Returns HTTP `201` with `{ "success": true, "data": { ...transaction } }` — the raw transaction record, not the PascalCase shape of `/transaction`. Check `data.status` (`approved` / `declined`).

---

## Charge a Tokenized Bank Account — Public API

The ACH counterpart of `/token-transaction`: charge a `pci_ach_…` token from `/tokenize-ach`. Requires a server-side API key — SDK public keys (`lpk_live_…`) get `403 FORBIDDEN`.

### Endpoint

```http
POST /api/v1/public/token-transaction-ach
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/public/token-transaction-ach \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "pci_ach_4b7d2e91c0aa",
    "amount": 250.00,
    "description": "Invoice 1042",
    "billingEmail": "john.doe@example.com"
  }'
```

| Field | Required | Description |
|-------|----------|-------------|
| `token` | **Yes** | `pci_ach_…` token (`pciAchToken` / `tokenId` also accepted) |
| `amount` | **Yes** | Amount (> 0) |
| `accountType`, `accountHolderName` | No | Override what the vault stored |
| `secCode`, `customerType`, `businessName` | No | ACH entry details |
| `customerId`, `description`, `orderId`, `billingEmail`, `billingPhoneNumber`, `billingFirstName`, `billingLastName` | No | Reference and billing data |
| `convenienceFeeActive` | No | Who pays the technology fee. Omit for the merchant's default payer; `true` — the customer pays it; `false` — the merchant absorbs it. Honoured when the merchant's account allows changing who pays; otherwise it is ignored, the merchant's pricing decides, and the response carries the warning `payer_change_not_allowed` |

Returns HTTP `201` with the ACH transaction (`status: "pending"` while the debit settles). A tenant-scoped key without a resolvable merchant gets `400 VALIDATION_ERROR`.

---

## Rerun a Declined Transaction — Public API

> **Note:** `POST /api/v1/public/rerun` is not available. The route exists in the gateway but the transaction service has no rerun operation, so it returns `404`. To retry a declined payment, submit a new `POST /api/v1/public/transaction`.

---

## Admin API Routes (Dashboard / JWT Auth)

These routes are used by the AuxCORE dashboard and require a JWT Bearer token. They accept camelCase request bodies and respond with `{ success, data }`, where `data` is the transaction record.

### Process Sale

```http
POST /api/v1/transactions/sale
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/transactions/sale \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "X-Tenant-ID: YOUR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "amount": 150.75,
    "currency": "USD",
    "paymentMethod": "card",
    "pciToken": "pci_tok_8f2c1a9b7e4d",
    "billingCustomerName": "John Doe",
    "billingEmail": "john@example.com"
  }'
```

Returns HTTP `201`. `merchantId` and `amount` (> 0) are required. Use `POST /api/v1/transactions/auth` with the same body for an authorization only. An optional `Idempotency-Key` header de-duplicates retries for 24 hours.

#### Itemised sale (`lineItems`)

`sale`, `auth`, `token-sale`, `ach-sale` and `external-payment` accept an optional `lineItems`
array — the cart sold with the payment (catalog products and free-text misc lines):

```json
"lineItems": [
  { "productId": "3f2b8c1e-4d5a-4e6f-8a7b-9c0d1e2f3a4b", "name": "Diesel", "quantity": 2.5, "unit": "gallon", "unitPrice": 4.19 },
  { "type": "misc", "name": "Car wash", "quantity": 1, "unitPrice": 8.00 }
]
```

Each line: `type` (`product` | `misc`), `productId` (required for product, forbidden for misc),
`name`, `quantity` (> 0, up to 3 decimals), optional `unit`, `unitPrice` (2 decimals), optional
`lineTotal` (= quantity × unitPrice). The lines must add up to `baseAmount` (amount before tip, tax
and convenience fee) within $0.01, otherwise `400 INVALID_LINE_ITEMS` before anything is charged.
A cart holding more of a product than its `maxQuantity` (summed across lines) is refused with
`400 QUANTITY_LIMIT_EXCEEDED` — also before anything is charged, and not on `external-payment`; see
[Products](../invoicing/products.md), section "Per-order quantity limit".
The normalised cart is stored on the transaction and returned by `GET /transactions/:id` (not by the
list). Product lines count toward the product's `soldQuantity` once the sale is approved, and come
back out on a void, a full refund or an ACH return. Full contract:
[Device Integration](../mobile/device-integration.md), section "Itemised sales (POS cart)".
#### Optional immutable purchase snapshot

Mobile card sales authenticated with `X-Device-Token` may include `enhancedData` without
sending `requestOrigin`. The server derives `ipos` or `nearpay` from the trusted device
context; portal Take Payment calls continue to use their existing `take_payment` context.
The field is optional and never reaches a processor.

```json
{
  "enhancedData": {
    "source": "manual",
    "lines": [{
      "stableLineId": "mobile-line-1",
      "description": "Widget",
      "quantity": "1",
      "unitPrice": "100.00",
      "tax": "8.00"
    }]
  }
}
```

Use `{ "source": "invoice", "invoiceId": "…" }` for an invoice candidate. The invoice
is reloaded under the device's tenant and merchant scope. It is stored only when its full
server-authoritative total equals the charge excluding tip and customer/convenience fees;
otherwise the sale remains a normal Standard payment with no snapshot.

For manual snapshot lines, `quantity` may have up to three decimal places and `unitPrice` may
have up to two. The exact quantity × unit-price calculation must resolve to whole currency
minor units; the API rejects a non-representable line total rather than rounding it.

### Capture an Authorization

```http
POST /api/v1/transactions/:transactionId/capture
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/transactions/7c9e6679-7425-40de-944b-e07fc1f90ae7/capture \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "X-Tenant-ID: YOUR_TENANT_ID"
```

Captures the full authorized amount — the endpoint takes no body (partial capture is not supported). Requires the `transactions.capture` permission.

### Cancel Capture

```http
POST /api/v1/transactions/:transactionId/cancel-capture
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/transactions/7c9e6679-7425-40de-944b-e07fc1f90ae7/cancel-capture \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "X-Tenant-ID: YOUR_TENANT_ID"
```

### Cash Payment (Record a Cash Sale)

Record a cash transaction. No gateway is called — the transaction is immediately created with `status: "settled"` and `convenienceFee: 0`.

> **Note:** Cash is not gated by merchant or device configuration — any merchant can record a cash sale.

```http
POST /api/v1/transactions/sale
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/transactions/sale \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "X-Tenant-ID: YOUR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "amount": 55.00,
    "tipAmount": 5.00,
    "paymentMethod": "Cash",
    "currency": "USD",
    "billingCustomerName": "Jane Smith",
    "billingEmail": "jane.smith@example.com",
    "requestOrigin": "virtual-terminal"
  }'
```

#### Request Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `merchantId` | string (uuid) | **Yes** | Merchant ID |
| `amount` | number | **Yes** | Total amount including tip and tax. Must be > 0 |
| `paymentMethod` | string | **Yes** | `"Cash"` (case-insensitive; `"Check"` is treated the same way) |
| `tipAmount` | number | No | Tip amount. Default: `0` |
| `taxAmount` | number | No | Tax amount. Default: `0` |
| `baseAmount` | number | No | Amount before tip and tax. Default: `amount - tipAmount - taxAmount` |
| `currency` | string | No | ISO 4217 currency code. Default: `"USD"` |
| `deviceId` | string (uuid) | No | Device ID when transaction originates from a device |
| `branchId` | string (uuid) | No | Location the sale belongs to |
| `orderId` | string | No | Internal order reference |
| `requestOrigin` | string | No | Source of the transaction. Default `"virtual-terminal"` |
| `description` | string | No | Transaction description |
| `signature` | string | No | Customer signature, printed on the receipt |
| `billingCustomerName` | string | No | Customer name (or `billingFirstName` + `billingLastName`) |
| `billingEmail` | string | No | Customer email — used to send receipt if provided |
| `billingAddress` | string | No | Street address |
| `billingCity` | string | No | City |
| `billingState` | string | No | State |
| `billingPostalCode` | string | No | ZIP/postal code |
| `billingCountry` | string | No | Country |
| `billingPhoneNumber` | string | No | Phone number |

> **Do not send card or ACH fields** (`cardNumber`, `cardExpiry`, `accountNumber`, etc.) — they are ignored for Cash payments.

#### Response — 201 Created

```json
{
  "success": true,
  "data": {
    "id": "0b6f3c3e-9a51-4d1e-8f43-2f7b1c9d6e10",
    "transactionId": "CASH-1750300000000-A1B2C3D4",
    "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "amount": "55.00",
    "baseAmount": "50.00",
    "tipAmount": "5.00",
    "taxAmount": "0.00",
    "convenienceFee": "0.00",
    "currency": "USD",
    "type": "sale",
    "status": "settled",
    "paymentMethod": "Cash",
    "provider": "Cash",
    "gatewayName": "Cash",
    "billingCustomerName": "Jane Smith",
    "billingEmail": "jane.smith@example.com",
    "requestOrigin": "virtual-terminal",
    "createdAt": "2026-06-19T10:00:00.000Z",
    "updatedAt": "2026-06-19T10:00:00.000Z"
  }
}
```

#### Cash Payment Rules

| Rule | Behavior |
|------|----------|
| `status` | Always `"settled"` — no gateway processing |
| `convenienceFee` | Always `0` — fees do not apply to cash |
| `type` | Only `sale` is supported. A Cash `auth` (`/transactions/auth`) returns `400 VALIDATION_ERROR` |
| Void | Not supported for Cash (`400`). Use Refund instead |
| Refund | Full refund only — a partial amount returns `400` |
| Settlement batch | Cash transactions are **excluded** from settlement batches |
| Ledger | Cash transactions **are** posted to the ledger |
| Receipt email | Sent automatically if `billingEmail` is provided |

---

### Record a Credit Against a Transaction

```http
POST /api/v1/transactions/:transactionId/credit
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/transactions/7c9e6679-7425-40de-944b-e07fc1f90ae7/credit \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "X-Tenant-ID: YOUR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 50.00
  }'
```

Creates a `credit` transaction (`CREDIT-…`, `status: "approved"`) for the original transaction's merchant, gateway and location. **No gateway call is made** — this records the credit only. `amount` must be a positive number. Returns HTTP `201`. A `reason` field is accepted but not stored.

### Issue an Unlinked Credit

```http
POST /api/v1/transactions/credit
```

Pushes funds to a card through the gateway without referencing a prior sale (the Virtual Terminal "Credit" action). Takes the same body as Process Sale (`merchantId`, `amount`, card / `pciToken`, billing fields) and returns HTTP `201` with the `credit` transaction.

---

### Record an External SDK Payment (IPOS / NearPay)

Record a payment the **device already completed through an external provider SDK** (IPOS or NearPay). The provider SDK only carries **total + tip**, so the platform-computed fee breakdown (`convenienceFee`, `taxAmount`, and the derived `baseAmount`) never reaches the provider webhook. The device calls this endpoint right after the SDK returns success to push the full breakdown, keyed by the provider's `gatewayTransactionId`.

The endpoint is **idempotent and async-safe with the provider webhook** — either side may arrive first, both matched on `gatewayTransactionId`:

- **Webhook not seen yet** → a `pending` transaction is created; the webhook later finalizes the status (`approved` / `declined`).
- **Webhook already created the record** → only the platform fee breakdown is filled in; the webhook-owned `status`, card, and auth data are left untouched. A later webhook re-delivery will **not** overwrite the device-supplied fees.

`enhancedData` is optional immutable purchase context (the same manual-lines or invoice-ID
candidate accepted by mobile card sales). It is resolved after the fee components are
normalized and stored with the local transaction only; it is never submitted to IPOS,
NearPay, or another processor. A webhook-first row receives the snapshot only if it has none.
Repeating the same candidate is idempotent; a different valid candidate returns
`409 ENHANCED_DATA_SNAPSHOT_CONFLICT` and never overwrites the first snapshot.

```http
POST /api/v1/transactions/external-payment
```

**Auth:** JWT Bearer **plus** a device token. This is a **device-only** endpoint — `X-Device-Token` is required; `tenantId`, `merchantId`, `deviceId`, and `terminalId` are resolved from the device context, never from the body.

```http
Authorization: Bearer YOUR_JWT_TOKEN
X-Device-Token: DEVICE_TOKEN
X-Tenant-ID: YOUR_TENANT_ID
Content-Type: application/json
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/transactions/external-payment \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "X-Device-Token: DEVICE_TOKEN" \
  -H "X-Tenant-ID: YOUR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "gatewayTransactionId": "85635456265410555120260724032413",
    "paymentMode": "IPOS",
    "type": "sale",
    "amount": 108.00,
    "baseAmount": 100.00,
    "tipAmount": 5.00,
    "convenienceFee": 3.00,
    "taxAmount": 0.00,
    "currency": "USD",
    "cardLast4": "4242",
    "cardBrand": "Visa"
  }'
```

#### Request Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `gatewayTransactionId` | string | **Yes** | Transaction ID returned by the IPOS/NearPay SDK — the key the webhook is matched on |
| `paymentMode` | string | **Yes** | `"IPOS"` or `"NEARPAY"` |
| `type` | string | No | `"sale"` (default) or `"auth"` |
| `amount` | number | **Yes** | Total charged (`baseAmount + tipAmount + taxAmount + convenienceFee`). Must be > 0 |
| `baseAmount` | number | No | Amount before tip/tax/fee. Default: `amount - tipAmount - taxAmount - convenienceFee` |
| `tipAmount` | number | No | Tip amount. Default: `0` |
| `convenienceFee` | number | No | Platform-computed convenience fee. Default: `0` |
| `taxAmount` | number | No | Platform-computed tax. Default: `0` |
| `currency` | string | No | ISO 4217 currency code. Default: `"USD"` |
| `cardLast4` | string | No | Last 4 digits of the card (display only), from the SDK response |
| `cardBrand` | string | No | Card brand (e.g. `"Visa"`), from the SDK response |
| `invoiceNumber` | string | No | Linked invoice number |
| `enhancedData` | object | No | Immutable purchase candidate: `{ source: "manual", lines: [...] }` or `{ source: "invoice", invoiceId }` |
| `orderId` | string | No | Internal order reference |
| `metadata` | object | No | Provider-specific extra data |

> **Do not send** `tenantId`, `merchantId`, `deviceId`, or `status` — the server sets them (`status` is always `pending` on create). The stored `transactionId` is `IPOS-<gatewayTransactionId>` for IPOS and `NPAY-<gatewayTransactionId>` for NearPay.

#### Response — 201 Created (no webhook yet)

```json
{
  "success": true,
  "data": {
    "id": "5d1f0c7a-2b8e-4c3a-9e61-7a4f2d8b9c01",
    "transactionId": "IPOS-85635456265410555120260724032413",
    "status": "pending",
    "amount": "108.00",
    "baseAmount": "100.00",
    "tipAmount": "5.00",
    "convenienceFee": "3.00",
    "taxAmount": "0.00"
  }
}
```

#### Response — 200 OK (webhook already created it, fees filled in)

```json
{
  "success": true,
  "data": {
    "id": "5d1f0c7a-2b8e-4c3a-9e61-7a4f2d8b9c01",
    "transactionId": "IPOS-85635456265410555120260724032413",
    "status": "approved",
    "baseAmount": "100.00",
    "convenienceFee": "3.00",
    "taxAmount": "0.00"
  }
}
```

#### Error Responses

| HTTP | Code | Cause |
|------|------|-------|
| `400` | `VALIDATION_ERROR` | Missing `gatewayTransactionId`; `paymentMode` not in `IPOS`/`NEARPAY`; `amount` ≤ 0; `type` not in `sale`/`auth` |
| `400` | `DEVICE_CONTEXT_REQUIRED` | Called without a valid `X-Device-Token` |
| `401` / `403` | `INVALID_DEVICE_TOKEN` / `DEVICE_SUSPENDED` | Device token invalid, revoked, or device suspended |
| `409` | `ENHANCED_DATA_SNAPSHOT_CONFLICT` | Retry supplied different purchase details after a snapshot was stored |
| `500` | `INTERNAL_ERROR` | Unexpected backend error |

---

## AVS Response Codes

| Code | Meaning |
|------|---------|
| `Y` | Address and 5-digit ZIP match |
| `A` | Address matches, ZIP does not |
| `Z` | ZIP matches, address does not |
| `N` | Neither address nor ZIP match |
| `U` | Address information unavailable |
| `R` | Retry — system unavailable |
| `S` | Service not supported |

## CVV Response Codes

| Code | Meaning |
|------|---------|
| `M` | CVV matches |
| `N` | CVV does not match |
| `P` | CVV not processed |
| `S` | CVV should be on card but not provided |
| `U` | Issuer unable to process CVV |

---

## Error Handling

### Declined Transaction

Public API returns HTTP **402** for card-level declines. Response codes are normalized to the NMI convention (`100` = approved, `2xx` = issuer decline, `3xx` = gateway reject, `4xx` = processor error):

```json
{
  "success": false,
  "status": "declined",
  "status_code": 402,
  "message": "Insufficient funds",
  "data": {
    "TransactionId": "9a3e7c21-6f4b-4d8e-a2c5-1b0f9e8d7c6a",
    "Status": "9",
    "ResponseCode": "202",
    "ResponseMessage": "Insufficient funds"
  }
}
```

### PCI Scope Violation

```json
{
  "success": false,
  "error": {
    "code": "PCI_SCOPE_VIOLATION",
    "message": "PublicAPI.transaction: raw card data is not permitted in this environment. Collect card data via the PCI iframe/SDK and submit a pciToken only."
  }
}
```

### Validation Error

Gateway-level checks on `/void` and `/refund` return a string `error`:

```json
{
  "success": false,
  "error": "Transaction ID required",
  "message": "Please provide a transactionId in the request body"
}
```

Errors raised by the transaction service (amount, status rules, not found) use the structured shape:

```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Only approved transactions can be voided"
  }
}
```

---

## Next Steps

- [Process Refunds →](./refunds.md)
- [Set up Recurring Billing →](../recurring/subscriptions.md)
- [Customer Vault →](../reference/endpoints.md)
- [Configure Fraud Filters →](../fraud/avs.md)
- [View Error Codes →](../reference/error-codes.md)

---

**Questions?** Contact support@auxcore.net
