HomeGuidesReference
Log In
Guides

Process EBT SNAP refunds

Learn how to process partial refunds for online SNAP and EBT payments, including FNS requirements and developer implementation options.

A refund is a repayment to a customer after they return item(s) in an order. This guide provides detailed information on refunding EBT SNAP benefits.

The guide is divided into the following sections:


📘

Refund Receipt Details

For details on refund receipts, see the Forage receipts guide.

Refund scenarios

Item substitutions

An item substitution is when a lower-cost alternative replaces an item in the original order. A customer might initiate the substitution, or a retailer or delivery shopper might make the swap when they discover a price discrepancy, an inventory error, or that the item is out of stock. Weighted items can also result in item substitutions. If the fulfilled weight is lower than the ordered weight, then the difference may result in a refund.

Item removals

An item removal is when an item is removed from the order. A customer might remove it while the order is being fulfilled, or a retailer or delivery shopper might opt to remove an item if it’s out of stock and there’s no adequate substitute, or if the retailer doesn’t support substitutions.

Item returns

A customer might initiate a return if an item has expired or is otherwise unsatisfactory.

📘

Promotions And Refunds

Refunds may affect eligibility for promotions, such as free shipping. To maintain a good customer experience, consider honoring the original offer.

FNS requirements

Deadlines

  • Customer-requested refunds must be processed within two business days of receiving the returned item.
  • Refunds due to merchant error, such as overestimated weighted products, out-of-stock items, or substitutions, must be processed on the same day as the original order was fulfilled.
  • Merchants must send customers a refund notification, whether via email, a confirmation screen, or the customer’s account portal, within 24 hours of the refund being completed.

📘

Refund Receipt Requirements

For details and examples of refund notifications, see the Forage receipts guide.

Payment instruments

📘

FNS Refund Requirements Summary

Refunds must match original payment methods:

  • SNAP-ineligible items can’t be refunded to SNAP.
  • EBT Cash–ineligible items can’t be refunded to EBT Cash.
  • Never refund more than was originally charged.

SNAP

Only SNAP-eligible purchases can be refunded to a customer's SNAP balance. To protect against fraud, no other payment instrument can be used to receive the SNAP refund. Refunds from SNAP to credit/debit, EBT Cash, or store gift cards are prohibited.

Refunding non-SNAP items, like alcohol, should never result in a SNAP refund.

EBT Cash

Only EBT Cash-eligible purchases can be refunded to a customer's EBT Cash balance. To protect against fraud, no other payment instrument can be used to receive the EBT Cash refund. Refunds from EBT Cash to credit/debit, SNAP, or store gift cards are prohibited.

Refunding non-EBT Cash items, like alcohol, should never result in an EBT Cash refund.

Diagram showing correct and incorrect refund flows: non-SNAP items can be refunded to a credit card, but not to EBT; SNAP or EBT Cash items must be refunded to the EBT card, not to a credit card. Green arrows mark valid flows; red Xs mark prohibited ones.

SNAP purchases can only be refunded to SNAP. EBT Cash purchases can only be refunded to EBT Cash.

Refund entry

Every retailer must have a secure method for authorized employees to manually enter refund requests using password-protected user IDs. There must be functionality for an employee to either issue refunds by item or to directly enter a specific amount to be refunded.

Refund notification

Refer to the EBT SNAP refunds section of the receipts guide for a comprehensive list of information that must be shared with customers after a refund is completed.

How to process online EBT SNAP refunds

When a full order is refunded, the merchant returns the total amount paid by the customer. However, if only some items are refunded, then the merchant refunds just the cost of those specific items.

To illustrate the different options, consider the following example receipt. On this receipt, "SP" indicates that an item is SNAP-eligible. "EBT" indicates EBT Cash eligible.

Sample grocery receipt showing an order with SNAP, EBT Cash, and credit card payments. Items are labeled with SNAP (“SP”) or EBT eligibility, and taxes are applied. The example illustrates how partial refunds are calculated across multiple payment instruments for different items in the order.

The receipt illustrates an order involving multiple payment instruments.

Imagine the customer returns Item A, a $10.00 SNAP-eligible item.

  • The refunds flow that restores the original tender amount refunds $10.00 to the customer’s SNAP balance.
  • The flow that maximizes the credit/debit refund refunds $10.10 to the customer's credit card.
  • The flow that automatically maximizes SNAP uses the \refund_by_product endpoint in Fully Hosted Checkout and returns the optimal split without manual redistribution.

Therefore, to handle partial refunds, select one of these refund type methods:

  1. Restore the original tender amount
  2. Manually maximize the credit/debit return
  3. Automatically maximize SNAP (Fully Hosted Checkout only)

👉 Or, you can jump to the example API requests.


Method 1. Restore the original tender amount

If you map the exact tender amount paid for each item in your database, the quickest refund flow is to issue a refund based on the original payment split.

In this scenario, the following might occur:

  • If the customer returns Item A, a refund of $10.00 is issued to the customer’s SNAP balance.
  • If the customer returns both Item A and Item B, a refund of $10.00 is applied to the credit card balance.
  • If the customer returns Item D, a refund of $5.05 is applied to the customer’s EBT Cash account.

Method 2. Manually maximize the credit/debit return

To maximize the credit/debit return instead, you can redistribute the payment split.

Calculate the refund based on whether the restored EBT SNAP balance can be used for other eligible items in the cart. This step isn't mandatory, but it offers a better customer experience.

📘

Not required by FNS, but improves CX

This refund flow is optional for compliance but recommended for customer experience teams.

If you're using Fully Hosted Checkout, the refund_by_product endpoint automatically maximizes SNAP refunds.

When a customer pays for SNAP-eligible items using multiple payment methods, it's possible to redistribute the refund split. The final refunded amounts are calculated based on whether the restored SNAP balance can be applied to other eligible items in the order.

For example, items with higher tax rates can be refunded to SNAP first, reducing out-of-pocket taxes and increasing the customer’s credit/debit refund.

We've broken this into five steps below to show how to implement this logic manually.


Step 1: Calculate the updated order total

Subtract the cost of the returned item from the total of the original order.

In this scenario, you would subtract the $10.00 value of Item A from the $60.00 order total. This gives an updated order total of $50.00.


Step 2: Sort any remaining SNAP-eligible items by the highest tax rate

If SNAP-paid items are returned, the SNAP credit can be redistributed to remaining SNAP-eligible order items. To maximize a customer’s potential credit/debit card return, apply the SNAP credit to the highest-taxed items first.

In this scenario, after removing Item A, two SNAP-eligible items are in the cart:

  • Item C: $10.00, Tax rate 1%
  • Item B: $10.00, Tax rate 0%

Item C has a higher tax rate, so it’s sorted above Item B.


Step 3: Apply SNAP credit to SNAP-eligible items

Apply all of the returned SNAP credit to the remaining SNAP-eligible items in the cart, starting with the highest-taxed items.

If the credit exceeds the value of the remaining SNAP-eligible items, then return the excess SNAP amount to the customer’s SNAP balance.

In this scenario, returning Item A restores $10.00 in SNAP credit. That $10.00 is fully applied to Item C because its tax rate is higher than that of Item B.


Step 4: Distribute the original EBT Cash charge

The original EBT Cash payment can be used for any remaining items that are SNAP-eligible or EBT Cash-eligible. The order of the items doesn’t matter: unlike SNAP purchases, EBT Cash purchases are taxed.

If the original EBT Cash payment exceeds the cost of the remaining eligible items, refund the difference to the customer’s EBT Cash account balance.

In this scenario, the customer paid $5.05 using EBT Cash. That $5.05 is fully applied to the original Item D.


Step 5: Apply the original credit/debit card payment to outstanding items and fees

Apply the original credit/debit card payment to any outstanding items and fees.

If the payment exceeds the cost of the outstanding items, refund the difference to the customer’s credit/debit card. If the cost of the outstanding items exceeds the original payment, create a new order and charge the customer the remaining amount.

In this scenario, the following items remain in the cart:

  • Item B: $10.00, Tax rate 0%
  • Item E: $25.00 (plus 1% Tax rate: $25.25)

The original $45.35 credit/debit card payment is applied to the outstanding balance of $35.25. The remaining $10.10 will be refunded to the credit/debit card.


Method 3. Automatically maximize SNAP (Fully Hosted Checkout only)

If you're using Fully Hosted Checkout, the refund_by_product endpoint automatically uses the maximize-SNAP logic. This means you don't have to manually perform the redistribution steps (Method 2, above).

⚠️

Important

This automatic behavior is available only on the refund_by_product endpoint. Other refund endpoints will not automatically maximize SNAP.

For details and request/response examples, see the Refund specific products in an Order section below.

Query the Forage API

The endpoints that you need to call to issue refunds differ depending on whether you’ve built with an SDK or the Forage Checkout UI. SDK integrations create PaymentRefunds, while Fully Hosted and Custom integrations create OrderRefunds.

How to handle a failed refund

Regardless of your integration type, if a refund attempt fails, retry the request with the same payload and the same idempotency key.

SDK

SDK integrations call the Payment Refunds endpoints.

To refund a payment, send a POST to /payments/{payment_ref}/refunds/, passing the ref for the relevant Payment to be refunded as the path param. In the request body, specify the amount to be refunded, along with a reason, as shown in the following example.

Request

curl --request POST \
     --url https://api.sandbox.joinforage.app/api/payments/{payment_ref}/refunds/ \
     --header 'Authorization: Bearer <authentication-token>' \
     --header 'Idempotency-Key: <idempotency-key>' \
     --header 'Merchant-Account: <merchant-account>' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "amount": 25.99,
  "reason": "Order could not be delivered"
}
'

Response

{
  "ref": "0ddbcda871",
  "payment_ref": "b873fe62dc",
  "merchant": "9000055",
  "funding_type": "ebt_snap",
  "amount": "25.99",
  "reason": "Order could not be delivered.",
  "created": "2023-09-18T11:38:29.279185-07:00",
  "updated": "2023-09-18T11:38:29.279209-07:00",
  "status": "requires_confirmation",
  "last_processing_error": null,
  "receipt": null
}

For more information, see these API reference docs:

Forage Checkout (Fully Hosted and Custom)

Forage Checkout integrations, for both Fully Hosted and Custom, call the Order Refunds endpoints.

Call different endpoints to refund an entire Order or refund part of an Order.

Refund an entire Order

To refund an entire Order, send a POST to /orders/{order_ref}/refunds/, passing the ref for the relevant Order to be refunded as the path param. In the request body, specify the reason for the refund and a metadata object, as in the following example.

Request
curl --request POST \
     --url https://api.sandbox.joinforage.app/api/orders/{order_ref}/refund_all/ \
     --header 'Authorization: Bearer <authentication-token>' \
     --header 'Idempotency-Key: <idempotency-key>' \
     --header 'Merchant-Account: <fns-number>' \
     --header 'accept: application/json' \
     --header 'content-type: application/json'
     --data '
{
  "reason": "Order could not be delivered",
  "metadata": {}
}
'
Response
{
  "ref": "e0f7607d5f",
  "snap_total": "10.00",
  "ebt_cash_total": "9.00",
  "remaining_total": "0.00",
  "product_list": [],
  "status": "draft",
  "delivery_address": {
    "city": "San Francisco",
    "country": "US",
    "line1": "1856 Market St.",
    "line2": "Apt. 3",
    "state": "CA",
    "zipcode": "94105,"
  },
  "is_delivery": false,
  "success_redirect_url": "",
  "cancel_redirect_url": "",
  "supported_benefits": [
    "snap",
    "ebt_cash",
    "non_ebt"
  ],
  "success_date": null,
  "receipt": null,
  "customer_id": null,
  "is_commercial_shipping": null,
  "expires_at": "2023-09-29T18:33:28.315051Z",
  "psp_customer_id": null,
  "external_order_id": null,
  "payments": [],
  "refunds": []
}

For more information, see these API reference docs:

Refund part of an Order

To refund part of an Order, specifically part or all of a single OrderPayment associated with an Order, send a POST to /orders/{order_ref}/refunds/, passing the ref for the relevant Order to be refunded as the path param. In the request body set the amount to be refunded, the ref for the relevant OrderPayment, the reason for the refund, and a metadata object, as in the following example.

Request
curl --request POST \
     --url https://api.sandbox.joinforage.app/api/orders/{order_ref}/refunds/ \
     --header 'Authorization: Bearer <authentication-token>' \
     --header 'Idempotency-Key: 123e4567e8' \
     --header 'Merchant-Account: <fns-number>' \
     --header 'accept: application/json' \
     --header 'content-type: application/json'
     --data '
{
  "amount": 25.99,
  "payment": <payment-ref>,
  "reason": "Order could not be delivered",
  "metadata": {}
}
'
Response
{
  "ref": "1ccabfg754",
  "order": "e0f7607d5f",
  "payment": "f587edf124",
  "merchant": "9000055",
  "funding_type": "ebt_snap",
  "amount": "20.00",
  "merchant_fixed_settlement": 5.95,
  "platform_fixed_settlement": 5.11,
  "reason": "Order could not be delivered.",
  "metadata": {},
  "created": "2023-09-18T11:38:29.279185-07:00",
  "updated": "2023-09-18T11:38:29.279209-07:00",
  "status": "requires_confirmation",
  "last_processing_error": null,
  "receipt": null,
  "tpp_lookup_id": "re_3JemUSGfBYJeLEva0af6My64",
  "refund_errors": []
}

Refund specific products in an Order

You can now refund individual products within an Order using the POST /orders/{order_ref}/refund_by_product/ endpoint.
This is useful when a refund applies to only certain items, rather than the entire order or a specific payment amount.

⚠️

Server-side only

For security, all requests to this endpoint must be made from your server. Do not call this endpoint from client-side code.

Request

Send a POST to /orders/{order_ref}/refund_by_product/, passing the order_ref of the relevant Order as the path parameter.
The request body must include:

  • products[] – A list of the products that need to be returned:
    • id (number) – A unique identifier for the product.
    • quantity (number) – The number of the items of the product to be returned.
  • reason (string) – A string that describes why the refund is happening.
  • metadata (object) – Merchant-defined key–value pairs for additional context. Pass {} if not applicable.

Example:

curl --request POST \
     --url https://api.sandbox.joinforage.app/api/orders/{order_ref}/refund_by_product/ \
     --header 'Authorization: Bearer <authentication-token>' \
     --header 'Idempotency-Key: <idempotency-key>' \
     --header 'Merchant-Account: <fns-number>' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "products": [
    {
      "id": 1235,
      "quantity": 2
    }
  ],
  "reason": "Product is damaged",
  "metadata": {}
}
'
Response

The response returns an array of refund objects, with each refund containing the following key details:

ObjectDescription
refA unique reference hash identifying this OrderRefund.
orderThe reference hash of the parent Order being refunded.
paymentThe reference hash of the specific OrderPayment being refunded.
merchantThe merchant’s identifier, such as their FNS number or a Forage-provided merchant ID.
funding_typeThe tender type for the refund; e.g. benefit, credit_tpp, ebt_snap, or ebt_cash.
amountThe refund amount in USD (decimal).
merchant_fixed_settlementFixed USD amount restored to the merchant for EBT Cash refunds, before fees.
platform_fixed_settlementFixed USD amount restored to the platform for EBT Cash refunds, before fees.
reasonA string explaining why the refund is being issued.
metadataOptional merchant-defined key–value pairs for storing extra information.
createdISO 8601 timestamp when the refund was created.
updatedISO 8601 timestamp when the refund was last updated.
statusCurrent refund status: canceled, failed, processing, or succeeded.
last_processing_errorAn object with code and message describing the latest Payments API error, or null.
receiptRefund receipt object for customer display; initially null until available.
tpp_lookup_idExternal ID from the third-party processor (e.g. Stripe Refund ID); null for EBT.
previous_errorsAn array of errors returned during refund creation; not present in GET responses.

Example:

{
  "ref": "1ccabfg754",
  "order": "e0f7607d5f",
  "payment": "f587edf124",
  "merchant": "9000055",
  "funding_type": "ebt_snap",
  "amount": "20.00",
  "merchant_fixed_settlement": 5.95,
  "platform_fixed_settlement": 5.11,
  "reason": "Order could not be delivered.",
  "metadata": {},
  "created": "2023-09-18T11:38:29.279185-07:00",
  "updated": "2023-09-18T11:38:29.279209-07:00",
  "status": "requires_confirmation",
  "last_processing_error": null,
  "receipt": null,
  "tpp_lookup_id": "re_3JemUSGfBYJeLEva0af6My64",
  "previous_errors": []
}

📘

Refund Status Updates

Use the REFUND_STATUS_UPDATED webhook to track changes in refund statuses.


For more information, see these API reference docs: