Quickstart
Use the prebuilt Forage Custom UI to collect a customer’s EBT Card PIN and process EBT payments in your website and app.
In this guide, you’ll learn how to:
- Create a Forage
PaymentMethod - Use the Forage Custom UI to check a
PaymentMethod's balance - Use the Forage Custom UI to process a payment
Prerequisites
- Sign up for a Forage account (contact us if you don’t yet have one)
- Register an app
- Create an authentication token
Step 1: Create a Forage PaymentMethod
PaymentMethod- Send a server-side
POSTto/payment_methods/.
After a customer enters their EBT Card number in your app, send a server-side POST to /payment_methods/ to tokenize the card, passing an EBT Card-specific request body. This request creates a Forage PaymentMethod, a representation of the card in Forage’s database.
curl --request POST \
--url https://api.sandbox.joinforage.app/api/payment_methods/ \
--header 'Authorization: Bearer <session-token>' \
--header 'Merchant-Account: <merchant-id>' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"card": {
"number": "<card-number>"
},
"type": "ebt",
"reusable": true,
"customer_id": "cus_1234567890"
}
'Example response:
{
"ref": "2aa1583cdb",
"type": "ebt",
"reusable": false,
"card": {
"last_4": "9999",
"created": "2024-02-06T12:59:43.147586-08:00",
"token": "xxx,yyy",
"state": null,
"fingerprint": "470dda97b63f016a962de150cf53ad72a93aaea4c2a59de2541e0994f48e02ef",
"fingerprint_v2": "a2f1b4e6c7d9380b59fa7c2e98ad0e316be9fcde1e2a4c3d56b17c6e0198a2fb"
},
"balance": null
}- Store the
refthat Forage returns in the response.
You’ll need the ref for the PaymentMethod to check the card’s balance and charge payments to it.
// replace with your database setup
const database = new Map()
// middleware to parse the request body
app.use(bodyParser.json())
app.post('/api/add_forage_payment_method', (req, res) => {
try {
const { ref } = req.body
// replace with your database logic
database.set(customerId, ref)
res.json({
success: true,
message: 'Forage PaymentMethod added successfully'
})
} catch (error) {
res.status(500).json({ success: false, error: 'Something went wrong' })
}
})# app.rb
require 'sinatra'
require 'net/http'
require 'json'
class ApiController < Sinatra::Base
post '/add_forage_payment_method' do
begin
ref = params[:ref]
# Replace the following logic with your ORM model or database setup
forage_payment_method = ForagePaymentMethod.new(customer_id: current_user.id, ref: ref)
if forage_payment_method.save
content_type :json
{ success: true, message: 'Forage PaymentMethod added successfully' }.to_json
else
status 500
{ success: false, error: 'Something went wrong' }.to_json
end
rescue StandardError => e
status 500
{ success: false, error: 'Something went wrong' }.to_json
end
end
end
PaymentMethod Creation DiagramFor a visual representation of how to create a Forage
PaymentMethodas part of a Custom integration, check out the diagram.
Step 2: Use the Forage Custom UI to check a PaymentMethod's balance
PaymentMethod's balance
FNS prohibits balance inquiries on sites that offer guest checkout.If a customer can check their card balance on your site, skip to payment capture. FNS prohibits balance inquiries on sites with guest checkout.
If guest checkout isn’t offered, adding a balance check is optional. No FNS regulations apply.
- Send a server-side
POSTto/balance_sessions/.
Pass the ref for the PaymentMethod as the payment_method body parameter, along with two params that instruct Forage where to direct the customer after they complete the Custom UI flow:
success_redirect_url: The URL that Forage redirects your customer to if the balance inquiry is successfulcancel_redirect_url: The URL that Forage redirects your customer to if the balance inquiry is canceled
curl --request POST \
--url https://api.sandbox.joinforage.app/api/balance_sessions/ \
--header 'Authorization: Bearer <token>' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'Merchant-Account: <merchant-account>' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"payment_method": "485dffb5b0",
"success_redirect_url": "https://www.your-app.com/status=SUCCEEDED",
"cancel_redirect_url": "https://www.your-app.com/status=CANCELED"
}
'Example response:
{
"payment_method": "485dffb5b0",
"success_redirect_url": "https://www.your-app.com/status=SUCCEEDED",
"cancel_redirect_url": "https://www.your-app.com/status=CANCELED",
"ref": "93410bcaff",
"redirect_url": "https://checkout.sandbox.joinforage.app/balance?session=93410bcaff&merchant=1234567"
}The Forage response includes a redirect_url that launches the front-end, customer-facing Forage Custom UI that prompts for a PIN to check the balance of a card. You need this value in the next step.
- Redirect the customer on the front-end to the
redirect_urlreturned from the POST to/balance_sessions/.
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { createBalanceSession } from 'my-api';
const MyComponent = () => {
const navigate = useNavigate();
const redirectToBalanceSession = () => {
const { redirect_url } = createBalanceSession();
navigate(redirect_url);
};
return (
<div>
<p>Your component content here</p>
<button onClick={redirectToBalanceSession}>Redirect to Balance URL</button>
</div>
);
};
export default MyComponent;The customer follows the Forage Custom UI prompts to input their card PIN to check its balance. If the check is successful, then Forage redirects the customer to the success_redirect_url. If unsuccessful, as in the case of a canceled balance check, Forage redirects them to the cancel_redirect_url.
How to override URL redirects on mobile
If your application deploys as a mobile app, then you will need to catch and override the URL redirects to keep the customer experience within the app. While you might have your own solution already, consider our recommendations:
iOS
Use the navigation delegate property of WKWebView. When the WebView begins to redirect, inspect the URL to determine if it is either the success_redirect_url or the cancel_redirect_url. In either case, close the WebView and proceed through each corresponding flow. Check out this discussion on implementation details for more information.
import UIKit
import WebKit
class YourViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// Initialize your WKWebView
webView = WKWebView(frame: view.frame)
webView.navigationDelegate = self
// Load the initial URL
if let url = URL(string: "https://checkout.sandbox.joinforage.app/balance?session=93410bcaff&merchant=1234567") {
let request = URLRequest(url: url)
webView.load(request)
}
view.addSubview(webView)
}
// Implement the WKNavigationDelegate method to handle navigation events
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// Inspect the URL during redirection
if let url = navigationAction.request.url {
let urlString = url.absoluteString
// Check if the URL matches the success or cancel redirect URLs
if urlString == "https://www.your-app.com/status=SUCCEEDED" {
// Close the WebView and proceed with success flow
} else if urlString == "https://www.your-app.com/status=CANCELED" {
// Close the WebView and proceed with cancel flow
} else {
// Close with error handling
}
} else {
// Close with error handling
}
decisionHandler(.cancel)
dismiss(animated: true, completion: nil)
}
}Android
We recommend using the shouldOverrideUrlLoading function. Follow the same decision tree described above. More information on implementation can be found here.
import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
class YourWebViewActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(<layout_resource_id>) // Replace with the actual layout resource ID
webView = findViewById(<webview_id>) // Replace with the actual WebView ID in your layout
webView.webViewClient = MyWebViewClient()
// Load the initial URL
webView.loadUrl("https://checkout.sandbox.joinforage.app/balance?session=93410bcaff&merchant=1234567")
}
private inner class MyWebViewClient : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url.toString()
if (url == "https://www.your-app.com/?status=SUCCEEDED") {
handleSuccessRedirect()
} else if (url == "https://www.your-app.com/status=CANCELED"){
handleCancelRedirect()
} else {
// Handle error case
}
return false // Cancel the navigation
}
}
private fun handleSuccessRedirect() {
// Your success handling logic goes here
// Finish the activity or navigate back, etc.
finish()
}
private fun handleCancelRedirect() {
// Your cancel handling logic goes here
// Finish the activity or navigate back, etc.
finish()
}
}- Send a server-side
GETto/payment_methods/{payment_method_ref}/to retrieve the customer’s balance.
The Forage PaymentMethod object stores the balance of the card. Pass the ref for the PaymentMethod in the body of the request:
curl --request GET \
--url https://api.sandbox.joinforage.app/api/payment_methods/485dffb5b0/ \
--header 'Authorization: Bearer <session-token>' \
--header 'Merchant-Account: <merchant-id>' \
--header 'accept: application/json' \Example response:
{
"ref": "485dffb5b0",
"type": "ebt",
"reusable": true,
"balance": {
"snap": "25.99",
"non_snap": "10.99",
"updated": "2021-06-16T00:11:50.000000Z-07:00"
},
"card": {
"last_4": "3456",
"created": "2021-06-16T00:11:50.000000Z-07:00",
"token": "tok_sandbox_12345678901234567890",
"state": "CA"
}
}Retrieve the balance returned in the response and display it to the customer on the front-end.
Card Balance Check DiagramFor a visual representation of how to check the balance of a card as part of a Custom integration, check out the diagram.
Step 3: Use the Forage Custom UI to process a payment
- Send a server-side
POSTto/capture_sessions/.
This request creates a Forage Order , a representation in Forage’s database of the customer’s checkout interaction.
Check out the endpoint reference documentation for complete descriptions of the required parameters passed in the below example:
curl --request POST \
--url https://api.sandbox.joinforage.app/api/capture_sessions/ \
--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 '
{
"success_redirect_url" : "https://www.joinforage.com/status=SUCCEEDED",
"cancel_redirect_url" : "https://www.joinforage.com/status=CANCELED",
"delivery_address": {
"city": "San Francisco",
"country": "US",
"line1": "1857 Market St.",
"state": "CA",
"zipcode": "94117"
},
"is_delivery" : false,
"supported_benefits": ["snap", "ebt_cash"],
"payment_details": {
"snap_payment": {
"amount": 20.12,
"metadata": {},
"payment_method": "fsdf45345",
"description": "A SNAP payment",
"funding_type": "ebt_snap"
},
"ebt_cash_payment": {
"amount": 25.99,
"metadata": {},
"payment_method": "fsdf45345",
"description": "An EBT Cash payment",
"funding_type": "ebt_cash"
}
}
}Example response:
{
"status": "draft",
"delivery_address": {
"city": "San Francisco",
"country": "US",
"line1": "1857 Market St.",
"state": "CA",
"zipcode": "94117"
},
"is_delivery": false,
"payment_details": {
"snap_payment": {
"ref": "b29dj92e4g",
"amount": 20.12,
"metadata": {},
"payment_method": "fsdf45345",
"description": "A SNAP payment",
"funding_type": "ebt_snap"
},
"ebt_cash_payment": {
"ref": "c4gdj92e4g",
"amount": 25.99,
"metadata": {},
"payment_method": "fsdf45345",
"description": "An EBT Cash payment",
"funding_type": "ebt_cash"
}
},
"supported_benefits": [
"snap",
"ebt_cash"
],
"ref": "b873fe62dc",
"success_date": null,
"receipt": null,
"customer_id": null,
"is_commercial_shipping": null,
"success_redirect_url": "https://www.your-app.com/?status=SUCCEEDED",
"cancel_redirect_url": "https://www.your-app.com/?status=CANCELED",
"redirect_url": "https://checkout.sandbox.joinforage.app/payment?order=b873fe62dc&merchant=9000055"
}The response includes a redirect_url that launches the front-end, customer-facing Forage Custom UI that prompts for a PIN to capture a payment.
- Redirect the customer on the front-end to the
redirect_url.
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { createCaptureSession } from 'my-api';
const MyComponent = () => {
const navigate = useNavigate();
const redirectToCaptureSession = () => {
const { redirect_url } = createCaptureSession();
navigate(redirect_url);
};
return (
<div>
<p>Your component content here</p>
<button onClick={redirectToCaptureSession}>Redirect to Capture URL</button>
</div>
);
};
export default MyComponent;The customer follows the Forage Custom UI prompts to input their PIN to capture a payment. If the payment is successful, then Forage redirects them to the success_redirect_url. If unsuccessful, as in the case of a canceled payment, Forage redirects them to the cancel_redirect_url.
If you’re building a mobile integration, then you need to follow the steps to intercept the redirects to keep the experience in-app.
- Check the
Orderstatus.
To check the status of an entire order, send a GET to /orders/{order_ref}/:
curl --request GET \
--url https://api.sandbox.joinforage.app/api/orders/dee3c3045e/ \
--header 'Authorization: Bearer <authentication_token>' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'Merchant-Account: <merchant-account>' \
--header 'accept: application/json' \
--header 'content-type: application/json' \Example response:
{
"ref": "93c7dad6b5",
"funding_type": "ebt_snap",
"amount": "20.00",
"description": "EBT SNAP payment",
"metadata": {},
"payment_method": "485dffb5b0",
"created": "2023-10-05T17:38:26.698516-07:00",
"updated": "2023-10-05T17:38:26.698540-07:00",
"status": "requires_confirmation",
"last_processing_error": null,
"success_date": null,
"tpp_lookup_id": null,
"customer_id": null,
"external_order_id": null,
"refunds": [],
"order": "dee3c3045e"
}Inspect the status field in the response body. If the status is succeeded, then all OrderPayments have processed successfully. If the status is failed, then check the Order.receipt.message field for details about the error.
You can also inspect the outcome of a single OrderPayment via a GET to /orders/{order_ref}/payments/{payment_ref}/.
Retry After Failed TransactionIf a transaction fails, repeat the steps above. Create a new
Ordervia anotherPOSTto/capture_sessions/and a new correspondingOrderPayment.
Payment Processing DiagramFor a visual representation of how to process a payment as part of a Custom integration, check out the diagram.
Updated 1 day ago
