HomeGuidesReference
Log In
Guides

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:

  1. Create a Forage PaymentMethod
  2. Use the Forage Custom UI to check a PaymentMethod's balance
  3. Use the Forage Custom UI to process a payment

Prerequisites

Step 1: Create a Forage PaymentMethod

  1. 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
}
  1. 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

🚧

FNS 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.

  1. 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 successful
  • cancel_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.

  1. 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()
  }
}
  1. 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

  1. 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.

  1. 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.

  1. 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 another POST to /capture_sessions/ and a new corresponding OrderPayment.

📘

For a visual representation of how to process a payment as part of a Custom integration, check out the diagram.