WIC API Reference
Endpoint specs, action codes, error codes, transaction limits, and receipt requirements for the Forage WIC API.
This WIC Integration Guide is composed of three parts:
[ How WIC Payments Work | Integrate WIC with Forage | 👉 WIC API Reference ]
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 WIC-specific.
| 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 | ⚠️ WIC | Sync daily APL data, tag catalog by UPC/PLU. |
| Real-Time Eligibility | ⚠️ WIC | Call confirmation endpoint as cart changes. |
| Item Substitutions | ⚠️ WIC | Replace items during fulfillment with category matching. Requires item details with product_list[]. |
| Prescription Tracking | ⚠️ WIC | 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 deprecationForage plans to deprecate the existing Retrieve Card Details Endpoint used for SNAP and replace it with this
/program_detailsendpoint to accommodate both WIC and SNAP.
agency_id= APLThe
agency_idreturned during card tokenization (viacard.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) |
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]) |
{
"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. |
{
"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 for the sync procedure.
Endpoint
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/ 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. |
{
"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.
See Integrate WIC with Forage: 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) |
{
"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 |
{
"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 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 |
{
"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. |
{
"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 ParametersForage appends
balance_session_refas 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 |
{
"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 for the full procedure.
Endpoint
POST /api/payment_methods/<payment_method_ref>/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. |
{
"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 |
{
"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 for the full procedure.
Request
See 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. |
{
"funding_type": "wic",
"description": "WIC Payment for Test Customer",
"payment_method": "abcd1234"
}Response
Payment details echoed back. See Create a Payment for standard fields.
{
"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.
See Integrate WIC with Forage: Capture a WIC Payment for the full procedure.
Endpoint
POST /api/payments/<payment_ref>/capture_paymentRequest
| 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
priceto1andquantityto the total dollar amount. For example, $8.85 of bananas →"price": 1, "quantity": 8.85. Some CVB items require theplufield instead ofgtin.
{
"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) |
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" |
{
"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 code26.- 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.
// Request
{
"product_list": [{ "name": "Low fat milk", "gtin": 12345666, "price": 5.0, "quantity": 1 }]
}// 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.
// 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 }]
}// 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.
// Request — price of $35 exceeds the NTE limit
{
"product_list": [{ "name": "Low fat milk", "gtin": 12345666, "price": 35.0, "quantity": 1 }]
}// 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".
// Request — a normal single-item request
{
"product_list": [{ "name": "Low fat milk", "gtin": 12345666, "price": 5.0, "quantity": 1 }]
}// 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.
// Request — 8 gallons requested, but participant only has 5 remaining
{
"product_list": [{ "name": "Low fat milk", "gtin": 12345666, "price": 7.0, "quantity": 8 }]
}// 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.
// Request
{
"product_list": [{ "name": "Infant Formula", "gtin": 485812345, "price": 11.0, "quantity": 1 }]
}// 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).
// Request
{
"product_list": [{ "name": "Soda", "gtin": 98765432, "price": 6.99, "quantity": 1 }]
}// 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.
// 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 }
]
}// 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.
See Integrate WIC with Forage: 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
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 |
// 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" |
{
"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 |
{
"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.
| 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 for worked examples of all discount scenarios.
Updated 2 days ago
