Custom 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
POST
to/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" # New field
},
"balance": null
}
- Store the
ref
that 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
For a visual representation of how to create a Forage
PaymentMethod
as part of a Custom integration, check out the diagram.
Step 2: Use the Forage Custom UI to check a PaymentMethod
's balance
PaymentMethod
's balanceFNS prohibits balance inquiries on sites that offer guest checkout.
Skip to payment capture if a customer can check their card balance on your site.
If your site doesn't offer guest checkout, then it's up to you whether or not to add a balance inquiry feature. No FNS regulations apply.
- Send a server-side
POST
to/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_url
returned 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
GET
to/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.
For 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
POST
to/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
Order
status.
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}/
.
If a transaction fails, then repeat the steps above. Create a new
Order
via anotherPOST
to/capture_sessions/
and a new correspondingOrderPayment
.
For a visual representation of how to process a payment as part of a Custom integration, check out the diagram.
Updated 6 months ago