> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brand-wallet.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Validate a transaction

> Dry-run a BrandWallet transaction: preview exactly what process would do — campaign results, rewards, promotions, milestones, and the amount to charge — without committing anything.

```http theme={null}
POST $BASE_URL/v1/transactions/validate
```

**Runs the full [process](/api/process-transaction) pipeline as a dry-run: the same body, the same validations, the same campaign / promotion / milestone math — and the same response shape. Nothing is committed: no balances change, no coupons are created or redeemed, no transaction is written.**

> **Auth:** `Authorization: Bearer <accessToken>` — an organization-scoped token from [Authentication](/api/authentication).

Use it to show the customer what a checkout **would** produce before confirming it: stamps or points they would collect, coupons they would earn, order discounts (`discountAmount`) and point spend applied, and the final `payableAmount`. Once confirmed, send the **same body** to [POST /v1/transactions/process](/api/process-transaction) to commit it — optionally echoing the previewed `payableAmount` as `expectedPayableAmount` so process fails safely (`payable_amount_mismatch`, no side effects) if anything changed in between.

### Body

Identical to [process](/api/process-transaction#body) — every field, including `items`, `redeem` (`coupons` + `points`), `orderId`, and `collectables` (escape hatch — avoid unless necessary; rules on the [process page](/api/process-transaction)), is accepted and evaluated the same way. See the process page for the field reference. The only exception is `expectedPayableAmount`: it belongs to process (validate is where you obtain the value) and is ignored here.

### Example

```bash theme={null}
curl --request POST \
  --url "$BASE_URL/v1/transactions/validate" \
  --header 'Authorization: Bearer <accessToken>' \
  --header 'Content-Type: application/json' \
  --data '{
    "customer": "3d8ec956-de2a-4d43-9c1e-74475b153362",
    "card": "41dd8263-288e-4b55-bf2e-6a82995a0441",
    "items": [
      { "id": "4221450", "name": "Americano", "quantity": 3, "amount": 50 }
    ],
    "amount": 150,
    "redeem": { "points": 120 }
  }'
```

### Response

Returns **200** with the same structure as [process](/api/process-transaction#response):

```json theme={null}
{
  "message": "You collected 3 stamps, earned a Free Coffee coupon. Amount to pay: 138",
  "executedActions": [
    {
      "actionName": "collect-stamps",
      "transactionCategory": "collect-stamps",
      "netCollected": 3,
      "previousCollectableValue": 2,
      "result": { "value": 0, "total": 15 },
      "rewards": ["9a417359-a02c-4a4a-8f4a-c31a24f0b0c5"],
      "milestones": [
        {
          "label": "15th purchase gift",
          "threshold": 15,
          "track": "total",
          "type": "issue-coupon",
          "triggerCount": 1,
          "result": null
        }
      ]
    }
  ],
  "executedPromotions": [
    {
      "promotion": "b34466d6-4e79-4523-aaf4-beaa672b3b8c",
      "name": "Weekend double coupon",
      "actionType": "issue_coupon",
      "coupons": [{ "coupon": "9a417359-a02c-4a4a-8f4a-c31a24f0b0c5" }]
    }
  ],
  "amount": 150,
  "discountAmount": 0,
  "payableAmount": 138,
  "redeemed": {
    "couponIds": ["2f19b6ba-e7f8-4dd5-b328-cbfabc08e412"],
    "autoAppliedCouponIds": [],
    "points": 120,
    "pointsValue": 12
  }
}
```

The field reference is the same as [process](/api/process-transaction#response). What differs is only what a dry-run *cannot* produce:

| Difference | In process | In validate |
| - | - | - |
| Reward coupon ids (`executedActions[].rewards`, `executedPromotions[].coupons[]`) | Coupon **instance** ids, created for the customer and usable in `redeem.coupons` later | Coupon **template** ids — they show *which* coupon would be earned, but no instance exists yet; do not pass them to `redeem.coupons` |
| `executedActions[].milestones[].result` | The executed effect's result | Always `null` — the crossing (`threshold`, `triggerCount`) is computed, the effect does not run |
| `redeemed.couponIds` | Coupons actually redeemed | Coupons that passed validation and **would** be redeemed |
| `redeemed.autoAppliedCouponIds` | Auto-apply coupons that were successfully redeemed | The customer's active auto-apply coupons, all assumed to succeed |
| Menu sync ([lazy sync](/api/process-transaction#menu-synchronization-lazy-sync)) | Unknown products/categories are created | Read-only: a product not yet known to BrandWallet is not created, so product-filtered campaigns may not match until the first committed transaction |
| User & branch sync ([lazy sync](/api/process-transaction#cashier-%26-branch-synchronization-lazy-sync)) | An unknown `user`/`branch` **object** with a `name` is created | Read-only: nothing is created, and this is **not** an error — the preview runs with the authenticated user (and branchless when the token has no branch), matching what process computes for the newly created records. A nameless unknown object still fails (`invalid_user` / `invalid_branch`), as it would on process |

Notes:

* Safe to repeat: sending the same body any number of times changes nothing.
* All process validations run **identically**, so validate surfaces the same **400** errors before you commit: `invalid_customer`, `invalid_user`, `invalid_branch`, `card_not_found`, `no_applicable_campaign`, `invalid_transaction`, `point_campaign_not_found`, `insufficient_collectable_balance`, `invalid_points_to_spend`, `invalid_amount_value`, `invalid_coupon`, `coupon_is_not_valid`, `_collectables_with_items` / `_collectables_with_redeem` / `invalid_collectables`, and — when `orderId` is sent and already used — `duplicate_transaction` (useful to detect "already processed" before retrying).
* `payableAmount` = `amount` − `discountAmount` − the monetary value of `redeem.points`; the previewed numbers match what process commits for the same body and state.
* `executedPromotions` is omitted when no promotion applies; promotion condition, limit, and cap checks are evaluated for real, but no usage is consumed by a validate call. Order-discount promotions (`apply_discount`) are previewed exactly like process, including their effect on `discountAmount` and `payableAmount`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.