# WIC API Reference Endpoint specs, action codes, error codes, transaction limits, and receipt requirements for the Forage WIC API. Complete technical reference for all WIC API endpoints, response codes, field definitions, and transaction rules. > See [Integrate WIC with Forage](./wic-integration.md) for step-by-step integration procedures. New to WIC? Read [How WIC Payments Work](./wic-payments.md) first. *** ## Integration Component Summary Your WIC integration builds on your existing EBT SNAP or EBT Cash work. Most components are shared and require only minor parameter changes; four are net new. | Integration Component | Status | Notes | | ---------------------- | ---------- | ----------------------------------------------------------------------------------------------------- | | Card Tokenization | ✅ Shared | Use `POST /api/payment_methods/` with `type: "wic"`. | | Balance Inquiry & PIN | ✅ Shared | Creates reusable session, returns prescription for WIC. | | Payment Capture | ✅ Shared | Same endpoint — add `product_list[]` for item details. | | Refunds | ✅ Shared | No PIN required, chains off original payment. Add `product_list[]` for item details. | | Payment Method Storage | ✅ Shared | Reusable tokens work identically. | | Inventory Management | ⚠️ Net New | Sync daily APL data, tag catalog by UPC/PLU. | | Real-Time Eligibility | ⚠️ Net New | Call confirmation endpoint as cart changes. | | Item Substitutions | ⚠️ Net New | Replace items during fulfillment with category matching. Requires item details with `product_list[]`. | | Prescription Tracking | ⚠️ Net New | Display category/subcategory balances, not dollar amounts. | *** ## GET /api/program\_details/ — Agency and Card Details WIC is administered by agencies—typically one per state, but tribal nations and territories have their own agencies. This endpoint returns EBT card details for each state as well, which is relevant to SNAP. > 📘 Planned deprecation > > Forage plans to deprecate the existing [Retrieve Card Details Endpoint](https://docs.joinforage.app/reference/retrieve-state-card-details) used for SNAP and replace it with this `/program_details` endpoint to accommodate both WIC and SNAP. > 📘 `agency_id` = APL > > The `agency_id` returned during card tokenization (via `card.state`) tells you which agency's APL applies to a given customer. ### Response (200 OK) | Field | Description | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `wic[]` | Array of WIC agency objects | | `wic[].agency_id` | Unique identifier for the WIC agency. Use this in subsequent API calls. | | `wic[].agency_name` | Human-readable agency name | | `wic[].bin` | Card BIN prefix that routes to this agency | | `wic[].pan_lengths` | Supported PAN lengths for this agency (e.g., `[16]` or `[16, 19]`) | | `wic[].broadband_straddle_allowed` | Whether sub-category straddle is permitted (see [How WIC Payments Work](./wic-payments.md#how-wic-straddle-rules-work)) | | `ebt[]` | Array of EBT state objects | | `ebt[].state` | Two-letter state code | | `ebt[].bin` | Card BIN prefix that routes to this state | | `ebt[].pan_lengths` | Supported PAN lengths for this state (e.g., `[16]` or `[16, 19]`) | ```json { "wic": [ { "agency_id": "040", "agency_name": "Massachusetts WIC", "bin": "610320", "pan_lengths": [16, 19], "broadband_straddle_allowed": false }, { "agency_id": "047", "agency_name": "Navajo Nation WIC", "bin": "610188", "pan_lengths": [16], "broadband_straddle_allowed": false } ], "ebt": [ { "state": "MA", "bin": "600528", "pan_lengths": [16, 19] }, { "state": "NJ", "bin": "610434", "pan_lengths": [16] } ] } ``` *** ## GET /api/wic/categories/ — WIC Food Categories Returns the category and subcategory structure for WIC eligible items. Use this to understand how prescription benefits are organized and to look up `category_description` values not included in APL responses. ### Response (200 OK) | Field | Description | | ------------------------------------------------------ | ------------------------------------------------------ | | `categories[]` | Array of WIC categories | | `categories[].category_code` | WIC category code | | `categories[].category_description` | Human-readable category name | | `categories[].subcategories[]` | Array of subcategories within this category | | `categories[].subcategories[].subcategory_code` | WIC sub-category code (`000` = Broadband) | | `categories[].subcategories[].subcategory_description` | Human-readable sub-category name | | `categories[].subcategories[].unit_of_measure` | Unit type: `GALLON`, `DOZEN`, `OUNCE`, `DOLLARS`, etc. | ```json { "categories": [ { "category_code": "03", "category_description": "Milk", "subcategories": [ { "subcategory_code": "001", "subcategory_description": "Whole Milk", "unit_of_measure": "GALLON" }, { "subcategory_code": "002", "subcategory_description": "2% Milk", "unit_of_measure": "GALLON" } ] }, { "category_code": "19", "category_description": "Fruits and Vegetables", "subcategories": [ { "subcategory_code": "001", "subcategory_description": "Fresh Fruits and Vegetables", "unit_of_measure": "DOLLARS" } ] } ] } ``` *** ## GET /api/wic/apl/ — Approved Product List Returns all UPC/PLU items eligible for WIC purchase within an agency. APLs refresh daily; sync this data with your product catalog on the same schedule. See [Integrate WIC with Forage: Sync APL Inventory Data](./wic-integration.md#sync-apl-inventory-data) for the sync procedure. ### Endpoint ```text GET /api/wic/apl/?agency_id={id}&cursor={cursor}&limit={limit}&since={date}&upc={str} ``` ### Request Parameters | Parameter | Type | Description | | ----------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `agency_id` | Optional string | The agency identifier (e.g., `040`). If provided, results are scoped to items present in the agency's APL. | | `cursor` | string | Pagination cursor (from `next` field in response) | | `limit` | Optional integer | Number of items per page (default: 1000, max: 5000) | | `since` | Optional string | If provided, only items with an updated timestamp ≥ this value are returned. Otherwise all items are returned. Supports up to 30 days of history; if your data is more stale, fetch all APL data. | | `upc` | Optional string | If provided, only the item matching this UPC is returned. | ### Response (200 OK) | Field | Description | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `next` | URL for next page of results, or `null` if no more pages | | `results[]` | Array of Products from WIC Agency APL files | | `results[].gtin` | The GTIN for a WIC item | | `results[].plu` | The PLU for an item | | `results[].eligible_agencies[]` | List of agency IDs the item is eligible for | | `results[].category_code` | WIC category code (match against prescription). To get `category_description`, call [GET /api/wic/categories/](#get-apiwiccategories--wic-food-categories) and look up by `[category_code][subcategory_code].category_description`. | | `results[].name` | Human-readable product description | | `results[].unit_of_measure` | The benefit accounting unit: `GALLON`, `DOZEN`, `OUNCE`, `DOLLARS`, etc. This is the unit the prescription tracks, not the product's package size. **Cash Value Benefits (CVB)**: Category 19 (Fruits and Vegetables) uses dollar-based benefits. For these items `unit_of_measure` is `DOLLARS`, `benefit_quantity` is `null`, and the prescription specifies a dollar amount rather than a quantity. | | `results[].benefit_quantity` | How many `unit_of_measure` units this product consumes from the benefit. A half-gallon of milk has `benefit_quantity: 0.5` because it consumes 0.5 gallons. `null` for CVB items. Different package sizes of the same product consume different amounts: a 1-gallon jug has `benefit_quantity: 1`, a half-gallon has `benefit_quantity: 0.5`. | ```json { "next": "?cursor=eyJndGluIjoiMDEyMzQ1Njc4OTAxIn0&limit=1000", "results": [ { "gtin": "012345678901", "name": "Whole Milk 1 Gallon", "category_code": "03", "subcategory_code": "001", "unit_of_measure": "GALLON", "benefit_quantity": 1, "eligible_agencies": [ "007": {"straddle": true}, "078": {"straddle": false} ] }, { "gtin": "011110089247", "name": "Whole Milk Half Gallon", "category_code": "03", "subcategory_code": "001", "unit_of_measure": "GALLON", "benefit_quantity": 0.5, "eligible_agencies": [ "007": {"straddle": true}, "063": {"straddle": false} ] }, { "plu": "4111", "name": "Fresh Bananas", "category_code": "19", "subcategory_code": "001", "unit_of_measure": "DOLLARS", "benefit_quantity": null, "eligible_agencies": [ "007": {"straddle": true}, "063": {"straddle": false}, "023": {"straddle": false} ] } ] } ``` *** ## POST /api/payment\_methods/ — Tokenize a WIC Card Tokenizes a WIC card for use in subsequent balance inquiries and payments. Uses the same endpoint as EBT SNAP tokenization—set `type` to `"wic"`. For test card numbers, see [test cards](https://docs.joinforage.app/docs/test-cards). See [Integrate WIC with Forage: Tokenize a WIC Card](./wic-integration.md#tokenize-a-wic-card) for the full procedure. ### Request | Field | Type | Description | | ------------- | ------- | ---------------------------------------------------------- | | `type` | string | Must be `"wic"` | | `reusable` | boolean | Set to `true` to reuse this payment method across sessions | | `card.number` | string | The full WIC card number (16–19 digits) | ```json { "type": "wic", "reusable": true, "card": { "number": "6104021234567890" } } ``` ### Response (201 Created) | Field | Description | | ------------------ | ------------------------------------------------------------------------------------ | | `ref` | Unique identifier for this payment method. Use this in subsequent API calls. | | `type` | The tender type of the Payment Method | | `reusable` | Whether or not the Payment Method can be used for subsequent purchases | | `card.last_4` | Last 4 digits of the card (for display purposes) | | `card.agency_id` | ID of WIC Agency this card belongs to. Determines which APL and benefit rules apply. | | `card.agency_name` | Human-readable WIC Agency Name | | `card.created` | Time the card was tokenized | ```json { "ref": "a1b2c3d4e5", "type": "wic", "reusable": true, "card": { "last_4": "7890", "agency_id": "007", "agency_name": "California WIC", "created": "2026-01-28T10:30:45Z" } } ``` *** ## POST + GET /api/balance\_sessions/ — Balance Inquiry Endpoints Two endpoints form the balance inquiry flow: POST to create the session, GET to retrieve the prescription after PIN entry. See [Integrate WIC with Forage: Perform a Balance Inquiry](./wic-integration.md#perform-a-balance-inquiry) for the full procedure. ### POST /api/balance\_sessions/ — Create a Balance Session #### Request | Field | Type | Description | | ---------------------- | ------ | ------------------------------------------------------------ | | `payment_method` | string | The `ref` returned when you created the payment method | | `success_redirect_url` | string | Where to redirect the customer after successful PIN entry | | `cancel_redirect_url` | string | Where to redirect if the customer cancels or PIN entry fails | ```json { "payment_method": "a1b2c3d4e5", "success_redirect_url": "https://your-app.com/wic/balance-success", "cancel_redirect_url": "https://your-app.com/wic/balance-cancel" } ``` #### Response (200 OK) | Field | Description | | ---------------------- | ------------------------------------------------------------------------------ | | `ref` | The balance session reference. Use this to retrieve results after PIN entry. | | `payment_method` | The payment method token associated with this balance check. | | `success_redirect_url` | The URL the customer is redirected to after successful PIN entry. | | `cancel_redirect_url` | The URL the customer is redirected to if they cancel PIN entry. | | `is_active` | Whether this session is still valid. | | `redirect_url` | Redirect the customer here for PIN entry. Forage handles PIN capture securely. | ```json { "ref": "93410bcaff", "payment_method": "a1b2c3d4e5", "success_redirect_url": "https://your-app.com/wic/balance-success", "cancel_redirect_url": "https://your-app.com/wic/balance-cancel", "is_active": true, "redirect_url": "https://checkout.joinforage.app/balance?session=93410bcaff&merchant=1234567" } ``` > 📘 Query Parameters > > Forage appends `balance_session_ref` as a query parameter to the redirect URL. If your URL already contains query parameters, Forage will append using `&` instead of `?`. ### GET /api/balance\_sessions/{ref}/ — Retrieve a Balance Session Read the `balance_session_ref` from the redirect query parameter, then call this endpoint to retrieve the customer's prescription. #### Response (200 OK) | Field | Description | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `ref` | The balance session reference. **Pass this to subsequent payment capture and order modification calls.** This links to the verified PIN and avoids requiring the customer to re-enter their PIN. | | `payment_method` | The payment method this session is associated with | | `pin_expires_at` | When this PIN-reuse session expires. You must capture the payment before this time. | | `benefit_end_date` | The last date these benefits can be used. Display this to the customer if they're close to expiration. | | `prescription[]` | Array of available benefits by category | | `prescription[].category_code` | WIC category code (use for APL matching) | | `prescription[].category_description` | Human-readable description of the category | | `prescription[].subcategory_code` | WIC sub-category code (use for APL matching) | | `prescription[].subcategory_description` | Human-readable description of the subcategory | | `prescription[].quantity_available` | Units available for quantity-based benefits (e.g., 3 gallons) and for CVB items (e.g., $3.00) | | `prescription[].unit_of_measure` | The unit of measure for a specific `(Category, SubCategory)` WIC prescription | ```json { "ref": "93410bcaff", "payment_method": "a1b2c3d4e5", "pin_expires_at": "2026-01-28T18:30:00Z", "benefit_end_date": "2026-02-28", "prescription": [ { "category_code": "03", "category_description": "Milk", "subcategory_code": "001", "subcategory_description": "Low Fat Milk", "quantity_available": 3.0, "unit_of_measure": "GALLON" }, { "category_code": "06", "category_description": "Cheese", "subcategory_code": "004", "subcategory_description": "Tofu", "quantity_available": 1.0, "unit_of_measure": "OZ" }, { "category_code": "31", "category_description": "Infant Formula", "subcategory_code": "001", "subcategory_description": "Nutramigen", "quantity_available": 11.0, "unit_of_measure": "LBS" } ] } ``` *** ## Balance Session Lifetime and Scope The balance session `ref` remains valid until `pin_expires_at`. During this window, you can: * Repeatedly request the cart's WIC Confirmation * Capture a payment * Modify the order during fulfillment All operations within the session use the originally verified PIN. The customer does not need to re-enter their PIN. *** ## POST /api/payment\_methods/{ref}/confirm/ — WIC Confirmation Returns a real-time breakdown of which items in the cart are covered by the customer's prescription. Call this endpoint on every cart change and again before checkout. See [Integrate WIC with Forage: Build the WIC-Eligible Cart](./wic-integration.md#build-the-wic-eligible-cart) for the full procedure. ### Endpoint ```text POST /api/payment_methods//confirm/ ``` ### Request | Field | Type | Description | | -------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `payment_method_ref` | string | The payment method ref involved in the Balance Inquiry. **Note**: The WIC balance must already be checked before calling `/confirm/`. | | `cart[].gtin` | string | Product GTIN for quantity-based WIC items | | `cart[].plu` | string | PLU for Cash Value Benefit (CVB) WIC items | | `cart[].name` | string | Product name | | `cart[].quantity` | number | Number of units. **CVB items** (Category Code `019`, e.g., Fruits & Vegetables): The per-unit price is $1.00, so `quantity` captures the *total dollar amount* for that UPC/PLU. | ```json { "payment_method_ref": "abcdef1234", "cart": [ { "gtin": "012345678901", "name": "Whole Milk 1 Gallon", "quantity": 2 }, { "gtin": "098765432109", "name": "2% Milk 1 Gallon", "quantity": 1 }, { "plu": "4011", "name": "Bananas", "quantity": 14.45 }, { "gtin": "049000042566", "name": "Coca-Cola 12oz", "quantity": 1 }, { "gtin": "011110089247", "name": "Whole Milk Half Gallon", "quantity": 1 } ] } ``` ### Response (200 OK) | Field | Description | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `payment_method` | Reference to the Payment Method | | `pin_expires_at` | Timestamp indicating when the PIN entry session expires | | `benefit_end_date` | Date when the current benefit period ends | | `benefit_usage[]` | Per-subcategory breakdown of benefits used and remaining based on covered items | | `benefit_usage[].category_code` | WIC category code (e.g., `"03"` for Milk) | | `benefit_usage[].category_description` | Human-readable category name | | `benefit_usage[].subcategory_code` | WIC subcategory code (`"000"` indicates broadband) | | `benefit_usage[].subcategory_description` | Human-readable subcategory name | | `benefit_usage[].unit_of_measure` | Unit for the quantity fields (e.g., `GALLON`, `DOLLARS`) | | `benefit_usage[].quantity_available` | Initial balance in the cardholder's prescription for this `(Category, Subcategory)` | | `benefit_usage[].redemption_quantity` | Quantity that will be redeemed for this subcategory | | `covered_items[]` | WIC-eligible items covered by the available prescription. An item may be partially covered; see `indicator`. | | `covered_items[].gtin` | The GTIN for a WIC item. One of `gtin` or `plu` will be present. | | `covered_items[].plu` | The PLU for a WIC item. Present instead of `gtin` for CVB items. | | `covered_items[].name` | Human-readable item name | | `covered_items[].quantity` | Quantity of this item anticipated to be covered by WIC if capturing this exact basket | | `covered_items[].indicator` | Always `covered` for items in this array | | `uncovered_items[]` | Items and quantities not anticipated to be fully covered by WIC benefits. For partial coverage, the covered portion appears in `covered_items[].quantity` and the uncovered portion appears here. | | `uncovered_items[].gtin` | The GTIN of the item. One of `gtin` or `plu` will be present. | | `uncovered_items[].plu` | The PLU of the item | | `uncovered_items[].name` | Human-readable item name | | `uncovered_items[].quantity` | Total quantity the prescription will not cover for this item | | `uncovered_items[].indicator` | Reason this quantity is not covered. Never `covered`. | **Item indicator values** | Value | Description | | ------------------- | ------------------------------------------------------------------------------------------------------- | | `covered` | This item is covered by the customer's WIC prescription | | `ineligible` | This item is not covered by WIC because it is not present in the WIC agency's APL | | `balance_exhausted` | This item is not covered by WIC because the cardholder has insufficient balance in that item's category | ```json { "payment_method": "a1b2c3d4e5", "pin_expires_at": "2026-01-28T18:30:00Z", "benefit_end_date": "2026-02-28", "benefit_usage": [ { "category_code": "03", "category_description": "Milk", "subcategory_code": "001", "subcategory_description": "Whole Milk", "unit_of_measure": "GALLON", "quantity_available": 2.0, "redemption_quantity": 2.0 }, { "category_code": "03", "category_description": "Milk", "subcategory_code": "000", "subcategory_description": "Broadband Milk", "unit_of_measure": "GALLON", "quantity_available": 1.0, "redemption_quantity": 1.0 }, { "category_code": "19", "category_description": "Fruits and Vegetables", "unit_of_measure": "DOLLARS", "quantity_available": 11.0, "redemption_quantity": 11.0 } ], "covered_items": [ { "gtin": "012345678901", "name": "Whole Milk 1 Gallon", "quantity": 2, "indicator": "covered" }, { "gtin": "098765432109", "name": "2% Milk 1 Gallon", "quantity": 1, "indicator": "covered" }, { "plu": "4011", "name": "Bananas", "quantity": 14.45, "indicator": "covered" } ], "uncovered_items": [ { "gtin": "049000042566", "name": "Coca-Cola 12oz", "quantity": 1, "indicator": "ineligible" }, { "gtin": "011110089247", "name": "Whole Milk Half Gallon", "quantity": 1, "indicator": "balance_exhausted" } ] } ``` *** ## POST /api/payments/ — Create a WIC Payment Uses the same endpoint as EBT SNAP payment creation. Set `funding_type` to `"wic"` and omit the `amount` field. WIC settlement amounts are determined at capture. See [Integrate WIC with Forage: Capture a WIC Payment](./wic-integration.md#capture-a-wic-payment) for the full procedure. ### Request See [Create a Payment](https://docs.joinforage.app/reference/create-a-payment) for the full request body. Note the following differences from SNAP: | Field | Changes for WIC | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `funding_type` | Must be set to `"wic"` | | `amount` | **Not required** for WIC payments. The settlement amount can change after capture due to price adjustments. Set on the payment after capture. | ```json { "funding_type": "wic", "description": "WIC Payment for Test Customer", "payment_method": "abcd1234" } ``` ### Response Payment details echoed back. See [Create a Payment](https://docs.joinforage.app/reference/create-a-payment) for standard fields. ```json { "ref": "6edd2dcda2", "funding_type": "wic", "description": "WIC Payment for Test Customer", "payment_method": "abcd1234", "created": "2026-01-22T15:38:20.648194-08:00", "status": "requires_confirmation", "expires_at": "2026-01-23T00:08:20.648216Z" } ``` *** ## POST /api/payments/{ref}/capture\_payment — Capture a WIC Payment Captures a WIC payment. Takes the covered items from the WIC Confirmation response, enriches them with shelf prices and any discounts, and submits them for processing. For the base capture pattern for EBT SNAP, see [server-side capture for EBT SNAP](https://docs.joinforage.app/docs/capture-ebt-payments-server-side). See [Integrate WIC with Forage: Capture a WIC Payment](./wic-integration.md#capture-a-wic-payment) for the full procedure. ### Endpoint ```text POST /api/payments//capture_payment ``` ### Request | Field | Description | | ------------------------- | ------------------------------------------------------------------------------------ | | `product_list[]` | Array of product items included in the request | | `product_list[].name` | Human-readable product name (e.g., `"Low fat milk"`, `"Bananas"`) | | `product_list[].gtin` | Global Trade Item Number (GTIN) identifying a packaged product (e.g., `12345666`) | | `product_list[].plu` | Price Look-Up code identifying a non-packaged item, typically produce (e.g., `4321`) | | `product_list[].price` | Unit price of the item in dollars (e.g., `5.00`) | | `product_list[].quantity` | Quantity of this item | > 📘 CVB Items (Fruits & Vegetables) > > CVB items use a different pricing model. Set `price` to `1` and `quantity` to the total dollar amount. For example, $8.85 of bananas → `"price": 1, "quantity": 8.85`. Some CVB items require the `plu` field instead of `gtin`. ```json { "product_list": [ { "name": "Low fat milk", "gtin": 12345666, "price": 5.0, "quantity": 1 }, { "name": "Bananas", "plu": 4321, "price": 1, "quantity": 8.85 } ] } ``` ### Response Standard payment fields echoed back, plus the following fields present only on captured payments: | Field | Type | Description | | ---------------------------------------- | ------- | ------------------------------------------------------------------------------------------------ | | `amount` | string | Total dollar amount to be settled by the WIC processor. Only present on `succeeded` responses. | | `status` | string | `"succeeded"`, `"failed"`, `"canceled"`, `"pending"` | | `product_list[]` | array | One entry per item in the original request | | `product_list[].name` | string | Product display name | | `product_list[].gtin` | integer | Global Trade Item Number. Echoed from request. | | `product_list[].plu` | integer | Price Look-Up code. Present instead of `gtin` for CVB items. | | `product_list[].requested` | object | Echo of the original request price and quantity | | `requested.price` | number | Price submitted in the request | | `requested.quantity` | number | Quantity submitted in the request | | `product_list[].approved` | object | Processor's decision for this item | | `approved.price` | number | Approved unit price. May be lower than requested due to NTE rules. `0` if declined. | | `approved.quantity` | number | Approved quantity. `0` if declined. | | `approved.action_code` | string | Processor result code (see [Capture Action Codes](#capture-action-codes-and-response-scenarios)) | | `approved.action_message` | string | Human-readable explanation of the decision | | `receipt` | object | WIC transaction receipt details to communicate to the WIC customer | | `receipt.prescription` | array | Current benefit balances after the transaction | | `prescription[].category_code` | string | WIC food category identifier | | `prescription[].category_description` | string | Human-readable category name | | `prescription[].subcategory_code` | string | Subcategory identifier within the category | | `prescription[].subcategory_description` | string | Human-readable subcategory name | | `prescription[].quantity_available` | number | Remaining units in this benefit subcategory | | `prescription[].unit_of_measure` | string | Unit type for `quantity_available` — e.g., `"GALLON"`, `"DOLLARS"` | ```json { "amount": "5.00", "status": "succeeded", "product_list": [ { "name": "Low fat milk", "gtin": 12345666, "requested": { "price": 5.0, "quantity": 1 }, "approved": { "price": 5.0, "quantity": 1, "action_code": "00", "action_message": "Approved" } } ], "receipt": { "prescription": [ { "category_code": "03", "category_description": "Milk", "subcategory_code": "001", "subcategory_description": "Low fat Milk", "quantity_available": 7.0, "unit_of_measure": "GALLON" } ] }, "ref": "6edd2dcda2", "funding_type": "wic", "description": "WIC Payment for Test Customer", "payment_method": "abcd1234", "created": "2026-01-22T15:38:20.648194-08:00", "status": "succeeded", "expires_at": "2026-01-23T00:08:20.648216Z" } ``` *** ## Capture Action Codes and Response Scenarios ### Overall Response Codes The top-level capture response indicates the transaction outcome: * **`000` — Full Success.** Every item was approved exactly as requested. Benefit changes occurred. * **`002` — Success with Price Adjustments.** The transaction succeeded and benefit changes occurred, but one or more items had NTE (Not to Exceed) price adjustments. The response includes those items with action code `26`. * **Any other code (e.g., `116`) — Failed.** No benefit changes occurred. This could be a top-level failure (e.g., invalid PIN) or one or more items declined. The response includes details on what failed. The response uses exception reporting: only items that need attention are returned. If an item is not in the response, it was approved as-is. ### Item-Level Action Codes | Code | Description | Suggested customer-facing message | | ---- | ---------------------------------------------------- | ------------------------------------------------------------------------------------ | | `00` | Approved | — | | `01` | Category not prescribed | "One or more items in your cart aren't covered by your WIC benefits." | | `02` | Sub-category not prescribed | "One or more items in your cart aren't covered by your WIC benefits." | | `03` | Insufficient units | "Your WIC benefits don't cover the full quantity of one or more items in your cart." | | `04` | UPC/PLU not prescribed | "One or more items in your cart aren't eligible for WIC." | | `26` | Approved for a lower price due to NTE price exceeded | "We adjusted one or more items in your cart to the maximum WIC-covered price." | ### Success Response Summary | Response | Status | Note | | ------------------------ | ----------- | -------------------------------------------------------------------- | | Full Approval (Quantity) | `succeeded` | — | | Full Approval (CVB) | `succeeded` | — | | Partial Approval (NTE) | `succeeded` | Item(s) exceeded the NTE limit and were approved at the capped price | | Straddle Applied | `succeeded` | Purchase draws from multiple benefit subcategories | ### Failure Response Summary | Response | Status | Likely Cause | Recommended Action | Suggested customer-facing message | | ----------------------- | -------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | Insufficient Balance | `failed` | Mismatch between the site's eligible items and the customer's actual prescription balance | Ask the customer to redo their balance check. If balance is accurate, the store's WIC catalog may be mislabeled. | "Your WIC benefits don't cover all the items in your cart. Please review your cart and try again." | | Category Not Prescribed | `failed` | The site has an item mapped to a benefit category the customer doesn't have | Verify the store's WIC item catalog. If catalog is correct, ask the customer to redo their balance check. | "One or more items in your cart aren't covered by your WIC benefits. Please remove them and try again." | | Item Not WIC Eligible | `failed` | Store's WIC catalog incorrectly flags a non-eligible item as eligible | Fix the item's WIC eligibility in the store catalog. | "One or more items in your cart aren't eligible for WIC. Please remove them and try again." | | Mixed Basket | `failed` | At least one item was declined, causing the entire WIC transaction to fail | Identify and resolve the specific item(s) that triggered the decline. | "Your WIC benefits couldn't cover one or more items in your cart. Please review your cart and try again." | | PIN Expired | `failed` | Too much time elapsed between the customer's PIN entry and the capture attempt | Ask the customer to re-enter their PIN and complete a new balance check. | "Your session has expired. Please re-enter your PIN and try again." | ### Full Approval — Quantity-Based Item A standard item is requested and approved without adjustments. The participant has sufficient balance in the relevant category. ```json // Request { "product_list": [{ "name": "Low fat milk", "gtin": 12345666, "price": 5.0, "quantity": 1 }] } ``` ```json // Response — approved.price and approved.quantity match the request { "amount": "5.00", "status": "succeeded", "product_list": [ { "name": "Low fat milk", "gtin": 12345666, "requested": { "price": 5.0, "quantity": 1 }, "approved": { "price": 5.0, "quantity": 1, "action_code": "00", "action_message": "Approved" } } ], "receipt": { "prescription": [ { "category_code": "03", "category_description": "Milk", "subcategory_code": "001", "subcategory_description": "Low fat Milk", "quantity_available": 7.0, "unit_of_measure": "GALLON" } ] } } ``` ### Full Approval — CVB Item (Cash Value Benefit) CVB items, such as fresh fruits and vegetables, are measured in dollars rather than units. ```json // Request — note: price is 1, quantity is the dollar amount, and plu is used instead of gtin { "product_list": [{ "name": "Bananas", "plu": 4566, "price": 1, "quantity": 8.85 }] } ``` ```json // Response { "amount": "8.85", "status": "succeeded", "product_list": [ { "name": "Bananas", "plu": 4566, "requested": { "price": 1, "quantity": 8.85 }, "approved": { "price": 1, "quantity": 8.85, "action_code": "00", "action_message": "Approved" } } ], "receipt": { "prescription": [ { "category_code": "08", "category_description": "Fruits and vegetables", "subcategory_code": "002", "subcategory_description": "Fresh", "quantity_available": 3.0, "unit_of_measure": "DOLLARS" } ] } } ``` ### Partial Approval — NTE Price Adjustment The requested price exceeds the Not-To-Exceed (NTE) limit for the item. The processor approves the item but reduces the price to the NTE ceiling. The quantity is unchanged. Your settlement amount should use `amount` from the response, not the original requested price. ```json // Request — price of $35 exceeds the NTE limit { "product_list": [{ "name": "Low fat milk", "gtin": 12345666, "price": 35.0, "quantity": 1 }] } ``` ```json // Response — price reduced from $35.00 → $7.00 (the NTE limit) { "amount": "7.00", "status": "succeeded", "product_list": [ { "name": "Low fat milk", "gtin": 12345666, "requested": { "price": 35.0, "quantity": 1 }, "approved": { "price": 7.0, "quantity": 1, "action_code": "26", "action_message": "Approved for a lower price due to NTE rules" } } ], "receipt": { "prescription": [ { "category_code": "03", "category_description": "Milk", "subcategory_code": "001", "subcategory_description": "Low fat Milk", "quantity_available": 5.0, "unit_of_measure": "GALLON" } ] } } ``` ### Special Approval — Straddle Applied A straddle occurs when a participant's specific subcategory balance is insufficient, so the processor pulls remaining units from the broadband subcategory (`000`). You can identify a straddle by checking `receipt.prescription` for multiple decremented entries, one of which has `subcategory_code: "000"`. ```json // Request — a normal single-item request { "product_list": [{ "name": "Low fat milk", "gtin": 12345666, "price": 5.0, "quantity": 1 }] } ``` ```json // Response — two prescription lines were affected { "amount": "5.00", "status": "succeeded", "product_list": [ { "name": "Low fat milk", "gtin": 12345666, "requested": { "price": 5.0, "quantity": 1 }, "approved": { "price": 5.0, "quantity": 1, "action_code": "00", "action_message": "Approved" } } ], "receipt": { "prescription": [ { "category_code": "03", "category_description": "Milk", "subcategory_code": "001", "subcategory_description": "Low fat Milk", "quantity_available": 0.0, "unit_of_measure": "GALLON" }, { "category_code": "03", "category_description": "Milk", "subcategory_code": "000", "subcategory_description": "Generic Milk Broadband", "quantity_available": 8.0, "unit_of_measure": "GALLON" } ] } } ``` ### Decline — Insufficient Balance The participant's remaining balance in the category cannot cover the requested quantity. The entire item is declined. ```json // Request — 8 gallons requested, but participant only has 5 remaining { "product_list": [{ "name": "Low fat milk", "gtin": 12345666, "price": 7.0, "quantity": 8 }] } ``` ```json // Response — action_code "03", approved price and quantity are both 0 { "status": "failed", "product_list": [ { "name": "Low fat milk", "gtin": 12345666, "requested": { "price": 7.0, "quantity": 8 }, "approved": { "price": 0.0, "quantity": 0, "action_code": "03", "action_message": "Insufficient benefit units available" } } ], "receipt": { "prescription": [ { "category_code": "03", "category_description": "Milk", "subcategory_code": "001", "subcategory_description": "Low fat Milk", "quantity_available": 5.0, "unit_of_measure": "GALLON" } ] } } ``` ### Decline — Category Not Prescribed The participant does not have a benefit prescription for the requested item's category. ```json // Request { "product_list": [{ "name": "Infant Formula", "gtin": 485812345, "price": 11.0, "quantity": 1 }] } ``` ```json // Response — action_code "01"; receipt still shows existing prescriptions, not the missing one { "status": "failed", "product_list": [ { "name": "Infant Formula", "gtin": 485812345, "requested": { "price": 11.0, "quantity": 1 }, "approved": { "price": 0, "quantity": 0, "action_code": "01", "action_message": "Category not prescribed" } } ], "receipt": { "prescription": [ { "category_code": "03", "category_description": "Milk", "subcategory_code": "001", "subcategory_description": "Low fat Milk", "quantity_available": 5.0, "unit_of_measure": "GALLON" } ] } } ``` ### Decline — Item Not WIC Eligible The product's UPC is not on the Approved Product List (APL). ```json // Request { "product_list": [{ "name": "Soda", "gtin": 98765432, "price": 6.99, "quantity": 1 }] } ``` ```json // Response — action_code "04" { "status": "failed", "product_list": [ { "name": "Soda", "gtin": 98765432, "requested": { "price": 6.99, "quantity": 1 }, "approved": { "price": 0.0, "quantity": 0, "action_code": "04", "action_message": "UPC not prescribed" } } ], "last_processing_error": { "code": "item_not_prescribed", "message": "Attempted to purchase an item that is not eligible according to the user's prescription", "source": { "resource": "Payments", "ref": "{payment_ref}" } } } ``` ### Decline — Mixed Basket If **any** item in the basket is not covered, the entire purchase fails—even items that would otherwise be approved. ```json // Request — milk is eligible, but infant formula is not prescribed for this participant { "product_list": [ { "name": "Low fat milk", "gtin": 12345666, "price": 5.0, "quantity": 1 }, { "name": "Infant Formula", "gtin": 485812345, "price": 11.0, "quantity": 1 } ] } ``` ```json // Response — status is "failed" even though milk was individually "Approved" { "status": "failed", "product_list": [ { "name": "Low fat milk", "gtin": 12345666, "requested": { "price": 5.0, "quantity": 1 }, "approved": { "price": 5.0, "quantity": 1, "action_code": "00", "action_message": "Approved" } }, { "name": "Infant Formula", "gtin": 485812345, "requested": { "price": 11.0, "quantity": 1 }, "approved": { "price": 0, "quantity": 0, "action_code": "01", "action_message": "Category not prescribed" } } ], "last_processing_error": { "code": "category_not_prescribed", "message": "Category of item is not in user's prescription", "source": { "resource": "Payments", "ref": "{payment_ref}" } }, "receipt": { "prescription": [ { "category_code": "03", "category_description": "Milk", "subcategory_code": "001", "subcategory_description": "Low fat Milk", "quantity_available": 5.0, "unit_of_measure": "GALLON" } ] } } ``` *** ## POST /payments/{ref}/refund/ — Refund and Substitution Handles fulfillment changes: partial refunds for out-of-stock or weight-adjusted items, full cancellations, and item substitutions. No PIN is required; all modifications chain off the original payment. For the base refund mechanics for EBT SNAP, see [EBT SNAP refunds](https://docs.joinforage.app/docs/ebt-refunds). See [Integrate WIC with Forage: Process Refunds and Item Substitutions](./wic-integration.md#process-refunds-and-item-substitutions) for the full procedure. ### Refund Types | Type | When to Use | Key Field | | -------------- | ---------------------------------------------------------------------- | -------------------------------------------- | | Full refund | Cancel the entire order before delivery | `"full_refund": true` | | Partial refund | Remove or adjust specific items (out of stock, damaged, weight change) | `product_list` with items to refund | | Substitution | Replace an item with an equivalent product | `substitute_with` on the item being replaced | ### Substitution Constraints Substitutions are more restrictive than plain refunds: * **Same category and subcategory** — store brand whole milk → name brand whole milk is valid; 2% milk → whole milk is not. * **Value cannot exceed original** — the substitute item's total value must be ≤ the original item's approved value. * **Requires an active card** — plain refunds work even if the card is deactivated, but substitutions require the card to remain active. ### Endpoint ```text POST /payments/{payment_ref}/refund/ ``` ### Request | Field | Type | Required | Description | | -------------------------------- | ------ | -------- | --------------------------------------------------------------------------------------- | | `product_list` | array | No | Items to refund or substitute. Required if `full_refund` is not set. | | `product_list[].gtin` | string | Yes | GTIN from the original payment. Use `plu` instead for CVB items. | | `product_list[].plu` | string | Yes | PLU from the original payment. Use instead of `gtin` for CVB items. | | `product_list[].quantity` | number | Yes | Units to refund. For CVB items, this is the dollar amount. | | `product_list[].price` | number | Yes | Must match the approved price from the original payment (including any NTE adjustment). | | `product_list[].reason` | string | Yes | Item-level reason: `out_of_stock`, `damaged`, `weight_adjustment` | | `product_list[].substitute_with` | object | No | Replacement item. Omit for a plain refund. | | `substitute_with.gtin` | string | Yes | Substitute product GTIN (or `plu` for CVB items) | | `substitute_with.plu` | string | Yes | Substitute product PLU. Use instead of `gtin` for CVB items. | | `substitute_with.quantity` | number | Yes | Substitute quantity (dollar amount for CVB items) | | `substitute_with.price` | number | Yes | Substitute unit price | ```json // Partial refund with substitution — include only the items being refunded or substituted { "product_list": [ { "gtin": "012345678901", "quantity": 1, "price": 5.99, "reason": "out_of_stock", "substitute_with": { "gtin": "012345678902", "quantity": 1, "price": 5.49 } }, { "plu": "0231", "quantity": 1.23, "price": 1, "reason": "out_of_stock", "substitute_with": { "plu": "0222", "quantity": 1.23, "price": 1 } } ] } ``` ### Response The response follows the same `product_list[].requested` / `product_list[].approved` pattern as capture payments. For substitutions, a `substituted_with` block appears on the affected item. | Field | Type | Description | | ------------------------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------ | | `ref` | string | Unique identifier for this refund transaction | | `payment_ref` | string | Reference to the original payment being modified | | `status` | string | `"succeeded"`, `"processing"`, or `"failed"` | | `amount` | number | Total dollar amount of the refund. For substitutions, reflects the net difference between original and substitute items. | | `product_list[]` | array | One entry per item in the refund request | | `product_list[].name` | string | Product display name | | `product_list[].gtin` | string | GTIN from the original payment. One of `gtin` or `plu` will be present. | | `product_list[].plu` | string | PLU from the original payment. Present instead of `gtin` for CVB items. | | `product_list[].requested` | object | The items in need of refunding or substitution | | `product_list[].requested.price` | number | Shelf price of the item to be refunded. Must match the original capture price. | | `product_list[].requested.quantity` | number | Quantity of this `gtin` or `plu` to refund or substitute | | `product_list[].approved` | object | Processor's decision for this item | | `product_list[].approved.price` | number | Approved refund price. `0` if declined. | | `product_list[].approved.quantity` | number | Approved refund quantity. `0` if declined. | | `product_list[].approved.action_code` | string | Processor result code | | `product_list[].approved.action_message` | string | Human-readable explanation of the decision | | `product_list[].substituted_with` | object | Replacement item details. Only present if a substitution was requested and processed. | | `substituted_with.gtin` | string | GTIN of the substituted item. One of `gtin` or `plu` will be present. | | `substituted_with.plu` | string | PLU of the substituted item. Present instead of `gtin` for CVB items. | | `substituted_with.name` | string | Substitute product display name | | `substituted_with.requested` | object | Echo of the substitute item values sent to the processor | | `substituted_with.requested.price` | number | Requested substitute price | | `substituted_with.requested.quantity` | number | Requested substitute quantity | | `substituted_with.approved` | object | Processor's decision on the substitute item | | `substituted_with.approved.price` | number | Approved substitute price. `0` if declined. | | `substituted_with.approved.quantity` | number | Approved substitute quantity. `0` if declined. | | `substituted_with.approved.action_code` | string | Processor result code for the substitute | | `substituted_with.approved.action_message` | string | Human-readable explanation for the substitute decision | | `receipt` | object | Post-refund benefit state | | `receipt.prescription` | array | Updated benefit balances after the refund (unchanged if refund failed) | | `receipt.prescription[].category_code` | string | WIC food category identifier (e.g., `"03"` for Milk) | | `receipt.prescription[].category_description` | string | Human-readable category name | | `receipt.prescription[].subcategory_code` | string | Subcategory identifier within the category | | `receipt.prescription[].subcategory_description` | string | Human-readable subcategory name | | `receipt.prescription[].quantity_available` | number | Remaining units in this benefit subcategory | | `receipt.prescription[].unit_of_measure` | string | Unit type — e.g., `"GALLON"`, `"DOLLARS"` | ```json { "ref": "refund_abc123", "payment_ref": "pay_xyz789", "status": "succeeded", "amount": 6.22, "product_list": [ { "name": "Horizon Organic Whole Milk 1 Gallon", "gtin": "012345678901", "requested": { "price": 5.0, "quantity": 1 }, "approved": { "price": 5.0, "quantity": 1, "action_code": "00", "action_message": "Approved" }, "substituted_with": { "gtin": "012345678902", "name": "Store Brand Whole Milk 1 Gallon", "requested": { "price": 4.99, "quantity": 1 }, "approved": { "price": 4.99, "quantity": 1, "action_code": "00", "action_message": "Approved" } } }, { "name": "Brand A Strawberries", "plu": "0231", "requested": { "price": 1, "quantity": 1.23 }, "approved": { "price": 1, "quantity": 1.23, "action_code": "00", "action_message": "Approved" }, "substituted_with": { "plu": "0222", "name": "Brand B Strawberries", "requested": { "price": 1, "quantity": 1.23 }, "approved": { "price": 1, "quantity": 1.23, "action_code": "00", "action_message": "Approved" } } } ], "receipt": { "prescription": [ { "category_code": "03", "category_description": "Milk", "subcategory_code": "002", "subcategory_description": "Whole Milk", "quantity_available": 7.0, "unit_of_measure": "GALLON" }, { "category_code": "19", "category_description": "Fruits and vegetables", "subcategory_code": "002", "subcategory_description": "Fresh", "quantity_available": 0.0, "unit_of_measure": "DOLLARS" } ] } } ``` *** ## Refund Error Codes When a refund fails, the response includes either a top-level `errors` array (for validation failures caught before processing) or a `refund_errors` array alongside the `product_list` (for item-level failures from the processor). | Error Code | Cause | Resolution | | ------------------------------------------------ | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `refund_category_mismatch` | Substitute item is in a different WIC category or subcategory than the original | Choose a substitute within the same category and subcategory | | `item_refund_quantity_exceeds_original_quantity` | Refund quantity is greater than the originally purchased quantity | Reduce quantity to match or be less than the original | | `item_refund_price_mismatch` | Refund price doesn't match the approved price from the original payment | Use the `approved.price` from the original capture response, not the shelf price | | `refund_item_not_in_original_payment` | GTIN/PLU was not part of the original transaction | Verify the item identifier against the original payment's `product_list` | ```json { "ref": "refund_abc123", "payment_ref": "pay_xyz789", "status": "failed", "amount": 40.0, "product_list": [ { "name": "Horizon Organic Whole Milk 1 Gallon", "gtin": "012345678901", "requested": { "price": 4.0, "quantity": 10.0 }, "approved": { "price": 0, "quantity": 0, "action_code": "29", "action_message": "Quantity exceeds purchase quantity" } } ], "refund_errors": [ { "code": "item_refund_quantity_exceeds_original_quantity", "message": "Attempted to refund a greater quantity of an item than originally purchased", "source": { "resource": "Payments", "ref": "{payment_ref}" } } ] } ``` *** ## WIC Transaction Error Handling Rules Requests are generally **all-or-nothing**, except for NTE outcomes. If any item fails validation (invalid UPC, incorrect substitution subcategory, or substitution value exceeds the item value), the entire request fails. If substitution is not possible (e.g., a different subcategory is needed), refund without `substitute_with` and prompt the customer for a new `balance_session` to purchase the substitute separately. *** ## Transaction Limits * **Items per transaction:** 50 unique UPC/PLUs max * **Cards per transaction:** 1 WIC card only * **Minimum purchase:** None (cannot require minimums) * **Taxes on WIC items:** Exempt *** ## Timing Constraints * **Balance session validity:** Until `expires_at` (a configurable value, typically less than 24 hours) * **Payment capture:** Must occur before `expires_at` * **Order modifications:** Until delivery; no refunds after the customer receives items * **Benefit expiration:** Benefits cannot be used after `benefit_end_date` *** ## Receipt Data Requirements WIC requires specific data elements at multiple points in the customer's journey. Each column below maps to a distinct receipt moment. For how Forage structures and delivers receipt objects, see [receipt requirements](https://docs.joinforage.app/docs/receipts). | Data Element | Balance Inquiry | WIC Confirmation | Vendor Sales Receipt | | ------------------------ | --------------- | ---------------- | -------------------- | | PAN (Last 4 digits) | ✅ | — | ✅ | | Store Name/Address | ✅ | — | ✅ | | Date/Time | ✅ | — | ✅ | | WIC Benefit Expiration | ✅ | — | ✅ | | WIC Items Purchased | — | ✅ (Proposed) | ✅ (Final) | | Category Description | — | — | — | | Sub-Category Description | ✅ | ✅ | ✅ | | Item Name | — | ✅ | ✅ | | Remaining Balance | ✅ | — | ✅ | | Discounts Applied | — | — | ✅ | **Receipt types:** * **Beginning WIC Balance** — Generated at balance inquiry completion * **Real-Time Shopping WIC Confirmation** — Updated as the customer adds items to their cart * **Pre-Purchase WIC Confirmation** — Shown at checkout before payment * ***Tentative* WIC Sales Receipt** — Generated post-checkout, pre-fulfillment; still subject to order modifications * ***Finalized* WIC Sales Receipt** — Generated post-fulfillment; the WIC order cannot be modified after this point *** ## Pricing Rules: NTE and Split-Tender ### NTE (Not-to-Exceed) Adjustments Some items have maximum prices set by the state. If your submitted price exceeds the NTE limit, Forage automatically adjusts the approved amount down. The response includes the adjusted price in `approved.price` with `action_code: "26"`. Use `amount` from the response for settlement, not the originally requested price. ### Split-Tender * **CVB items only:** If the purchase exceeds the CVB dollar balance, the customer pays the difference with another payment method. * **Non-CVB items:** No split-tender. The item is either fully covered by WIC or not covered at all. *** ## Discount Calculation Rules Apply discounts first, then transact WIC. When sending requests to Forage, set `price` for each product to the net amount after discount. ### Who Benefits from Discounts? | Item Type | Who Saves Money | | :------------------------------- | :------------------------------------------------------- | | Non-CVB items (milk, eggs, etc.) | The state agency (lower reimbursement) | | CVB items (fruits & vegetables) | The participant (stretches their dollar benefit further) | See [Integrate WIC with Forage: Apply Discounts to WIC Transactions](./wic-integration.md#apply-discounts-to-wic-transactions) for worked examples of all discount scenarios. *** ## Related documentation * [WIC Integration Guide](./wic-overview.md) — guide set overview, audience, and prerequisites * [How WIC Payments Work](./wic-payments.md) — voucher model, payment lifecycle, and straddle rules * [Integrate WIC with Forage](./wic-integration.md) — step-by-step integration: APL sync, balance inquiry, cart, capture, refunds, and discounts