# Fee Preview API (Dual Pricing / POS)

Pre-sale price preview for mobile and POS integrators. Returns **posted cash price**, **credit (card) price**, and **ACH price** so the client can display lawful dual pricing before charging (required in jurisdictions such as New York).

**Last updated:** 2026-10-01

Two endpoints return the same pricing: the JWT route below (dashboard, mobile and POS apps) and the API-key route `GET /api/v1/public/fee-preview` for server and checkout integrations (see Fee Preview — Public API).

---

## Endpoint

```http
GET /api/v1/transactions/fee-preview
```

### Authentication

| Header | Required | Description |
|--------|----------|-------------|
| `Authorization` | Yes | `Bearer <user_jwt>` |
| `X-Tenant-ID` | Yes | Tenant slug or ID |
| `X-Device-Token` | Optional | Device context |

Merchant-scoped users always get their own merchant (any `merchantId` sent is ignored). Admin users must pass `merchantId`, otherwise `400 VALIDATION_ERROR` (`Merchant ID is required for admin users`). API-key integrations should call `GET /api/v1/public/fee-preview` instead.

---

## Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `merchantId` | UUID | Admin only | Merchant to preview |
| `baseAmount` | number | Yes* | Merchandise amount before tip and before lane adjustment |
| `amount` | number | Alias | Same as `baseAmount` |
| `tipAmount` | number | No | Tip (default `0`) |
| `transactionType` | string | No | `sale` (default) or `auth` |
| `branchId` | UUID | No | Branch-specific schedule |
| `configOnly` | boolean | No | When `true`, return display config only (no amount math) |

\*When `baseAmount` is missing or ≤ 0 (or `configOnly=true`) no error is returned — the response is the config-only shape with zeroed prices.

Whether customer fees apply is taken from the merchant's primary gateway (`serviceFeeActive`, default `true`); it cannot be overridden on this route.

---

## Response

```json
{
  "success": true,
  "data": {
    "baseAmount": 100,
    "tipAmount": 0,
    "cardPrice": 104,
    "cashPrice": 100,
    "achPrice": 100,
    "convenienceFee": 4,
    "pricingModel": "cashdiscount",
    "enforcementMode": "v5",
    "customerFeesEnabled": true,
    "dualPricingRequired": true,
    "showPreview": true,
    "source": "v5",
    "cashDiscount": {
      "cashPrice": 100,
      "cardPrice": 104,
      "adjustment": 4
    },
    "prices": {
      "cash": { "total": 100, "adjustment": 0, "label": "Cash price" },
      "card": { "total": 104, "adjustment": 4, "label": "Credit price" },
      "ach":  { "total": 100, "adjustment": 0, "label": "ACH price" }
    },
    "appliedRules": [
      { "feeCode": "CD_ADJ", "feeName": "Card Adjustment", "calc": "percent", "amount": 4 }
    ],
    "snapshotId": "9b2e4f61-0c3d-4a8e-b7f5-1d2c3e4f5a6b",
    "scheduleVersion": 3
  }
}
```

Also returned when applicable: `perTransactionFee` (tech-fee model), `canChangePayer` / `defaultPayer` (hybrid `freedom` model), and `feeWarning` (`{ code, message }`). `cardPrice` / `cashPrice` / `achPrice` are deprecated — use `prices.*.total`.

### Fields integrators should use

| Field | Use |
|-------|-----|
| `dualPricingRequired` | When `true`, POS **must** display cash and credit prices before payment |
| `prices.cash` / `prices.card` / `prices.ach` | Per-lane totals and suggested labels |
| `customerFeesEnabled` | `false` when merchant disabled customer-facing fees — all lanes equal |
| `enforcementMode` | `legacy` = V5 schedule not enforced; dual pricing only active under `shadow` or `v5` |
| `showPreview` | Whether to show any fee breakdown UI |

### Semantic model (not a “discount”)

- **Posted / cash price** = `baseAmount + tipAmount` (what the merchant advertises)
- **Credit price** = posted price + card adjustment (compiled from V5 `cashdiscount` schedule)
- **ACH price** = posted price under a V5 `cashdiscount` schedule (ACH is cash-equivalent). Under `enforcementMode: legacy` the legacy convenience fee is applied to the ACH lane as well, so always display `prices.ach.total` rather than assuming it equals the cash price

---

## Config-only probe

Check whether a merchant requires dual-pricing display before the operator enters an amount:

```
GET /api/v1/transactions/fee-preview?merchantId={id}&configOnly=true
```

Returns `dualPricingRequired`, `enforcementMode` and `customerFeesEnabled` (plus `pricingModel` when one is resolved) with zeroed price fields and `showPreview: false`.

---

## Fee Preview — Public API

```http
GET /api/v1/public/fee-preview
```

What the cardholder will actually be charged, for API-key integrations (checkouts, ERP). Runs the same pricing as the real sale.

```bash
curl "https://uat.auxcore.net/api/v1/public/fee-preview?amount=49.00&tipAmount=0&paymentMethod=card"   -H "x-api-key: YOUR_API_KEY"
```

Merchant comes from the API key; tenant-scoped / ERP keys send `X-Merchant-ID`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `amount` | number | **Yes** | Base amount (> 0) — the same number you send to `/transaction`, never the fee-inclusive total. `baseAmount` is an alias |
| `tipAmount` | number | No | Tip (default `0`) |
| `taxAmount` | number | No | Tax (default `0`) |
| `paymentMethod` | string | No | `card` (default), `ach` or `cash` |
| `transactionType` | string | No | `sale` (default) or `auth` |
| `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`. Send the same value as the charge |
| `feeGatewayId` | string (uuid) | No | Gateway you will charge on — fee ceilings and floors are per gateway |
| `branchId` | string (uuid) | No | Branch-specific schedule |

```json
{
  "success": true,
  "data": {
    "baseAmount": 49,
    "convenienceFee": 1.47,
    "totalAmount": 50.47,
    "currency": "USD",
    "paymentMethod": "card",
    "convenienceFeeActive": true,
    "prices": {
      "cash": { "total": 49, "adjustment": 0, "label": "Cash price" },
      "card": { "total": 50.47, "adjustment": 1.47, "label": "Credit price" },
      "ach":  { "total": 49, "adjustment": 0, "label": "ACH price" }
    },
    "pricingModel": "cashdiscount",
    "dualPricingRequired": true
  }
}
```

`convenienceFee` and `totalAmount` are for the requested `paymentMethod`. `warning` (`{ code, message }`) is added when a fee was requested but the quote came back zero, or when `convenienceFeeActive` was ignored because the merchant's account does not allow changing who pays (`payer_change_not_allowed`).

| HTTP | Code | Cause |
|------|------|-------|
| `400` | `VALIDATION_ERROR` | `amount` missing or ≤ 0, or no merchant resolvable from the key |
| `401` | — | Missing or invalid API key |
| `502` | `FEE_PREVIEW_FAILED` | Pricing could not be calculated |

---

## Internal fee-config (service-to-service)

Served by the merchant service on its internal mount (not exposed through the API gateway):

```
GET /internal/merchants/:merchantId/fee-config
```

Headers: `x-internal-service-key`, `x-tenant-id`

Returns `cashDiscountEnabled`, `dualPricingRequired`, `v5PricingModel`, `enforcementMode`, and legacy `convenienceFee*` fields.

---

## POS integration checklist

1. On amount entry (or cart total change), call `fee-preview` with `baseAmount` and `tipAmount`.
2. If `dualPricingRequired`, render `prices.cash.label` + `prices.cash.total` and `prices.card.label` + `prices.card.total` (and `prices.ach` when ACH is offered).
3. When the customer selects a payment method, charge the matching `prices.{lane}.total`.
4. Do **not** compute adjustments client-side — always use this API so preview matches the sale path.
5. Dual pricing needs the merchant's pricing set to the cash discount model; `dualPricingRequired` is `true` when it is.

---

## Related endpoints

| Endpoint | Audience |
|----------|----------|
| `GET /api/v1/virtual-terminal/fee-preview` | Dashboard VT (single `paymentMethod`) |
| `GET /api/v1/public/fee-preview` | API-key integrations (single `paymentMethod`, per-lane `prices`) |
| `GET /api/v1/internal/fee-preview/at-charge` | Internal services (invoice, payment links); authenticated with `x-internal-secret` |

See [Mobile Device Integration](../mobile/device-integration.md) for device auth patterns.
