Quickstart
Use the Forage Terminal SDK to accept EBT SNAP and other payment methods via a POS system.
This guide explains how to get a foundational Forage POS Terminal Android app up and running. It also introduces the SDK's core payment operations. After reading the guide, you'll know how to complete the following steps:
- Set up a Forage Android app
- Add and style a Forage Element
- Configure a Forage Terminal instance
- Execute payment operations
Prerequisites
- Sign up for a Forage account (contact us if you don’t yet have one)
- Install Android API Level 23 or higher
- Install the following dependencies to copy/pasted code snippets and run the sample-app:
For more information, see:
- POS Terminal Android SDK reference docs
- Android SDK Quickstart (online-only)
- Payments API reference docs
Step 1: Set up a Forage Android app
Add an endpoint to your server that creates a session token.
A POST to the Forage /session_token/ endpoint creates a session token.
Environment Variable GuidelinesThese examples assume use of a
.envfile. Update them as needed, but never expose authentication tokens in client-side code..
// app.js
const axios = require('axios')
const express = require('express')
const bodyParser = require('body-parser')
require('dotenv').config();
const app = express()
const port = 5000
// Middleware to parse the request body
app.use(bodyParser.urlencoded({ extended: true }))
app.post('/api/session_token', async (req, res) => {
try {
const apiUrl = 'https://api.sandbox.joinforage.app/api/session_token/'
// Replace the below with your authentication token
const authenticationToken = process.env.AUTHENTICATION_TOKEN
// Replace the below with your Forage merchant ID
const merchantId = process.env.MERCHANT_ID
const headers = {
Authorization: `Bearer ${authenticationToken}`,
'Merchant-Account': merchantId,
'Content-Type': 'application/json'
}
const response = await axios.post(apiUrl, null, { headers })
res.json(response.data)
} catch (error) {
res
.status(error.response ? error.response.status : 500)
.json({ error: 'Something went wrong' })
}
})
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`)
})# app.rb
require 'sinatra'
require 'net/http'
require 'json'
require 'dotenv/load'
class ApiController < Sinatra::Base
post '/session_token' do
begin
api_url = 'https://api.sandbox.joinforage.app/api/session_token/'
# Replace the below with your authentication token
authentication_token = ENV['AUTHENTICATION_TOKEN']
# Replace the below with your Forage merchant ID
merchant_id = ENV['MERCHANT_ID']
uri = URI(api_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
headers = {
'Authorization' => "Bearer #{authentication_token}",
'Merchant-Account' => merchant_id,
'Content-Type' => 'application/json',
}
response = http.post(uri.path, nil, headers)
if response.is_a?(Net::HTTPSuccess)
json_response = JSON.parse(response.body)
content_type :json
json_response.to_json
else
status response.code
{ error: 'Something went wrong' }.to_json
end
rescue StandardError => e
status e.respond_to?(:status) ? e.status : 500
{ error: 'Something went wrong' }.to_json
end
end
endInstall the Android POS SDK from build artifacts.
-
Download the
forage-android-release.aarand thepom-default.xmlfiles from the latest release for the Forage Android POS Terminal SDK -
Add those files to a local directory called
libs/forage-android-pos-terminal-sdkinside your app’s directory.- You can put this elsewhere if you want, but you’ll need to adjust the paths in the gradle snippets later.
-
In your
build.gradle.ktsfile for your app (not the entire project), add this line to yourdependenciessection// adjust the path as needed depending on where you saved this in Step 2 implementation(files("libs/forage-android-pos-terminal-sdk/forage-android-release.aar")) -
To resolve the dependencies of the Forage POS SDK, you can either manually add them to your dependencies by reading the
<dependencies>section of thepom-default.xmlfile, or you can add this script to the end of yourbuild.gradle.kts, which will handle it for you —
import org.w3c.dom.Element
import javax.xml.parsers.DocumentBuilderFactory
// ...the rest of your existing build.gradle...
fun parsePomDependencies(pomFile: File): List<String> {
val dependencies = mutableListOf<String>()
val dbFactory = DocumentBuilderFactory.newInstance()
val dBuilder = dbFactory.newDocumentBuilder()
val doc = dBuilder.parse(pomFile)
val depNodes = doc.getElementsByTagName("dependency")
for (i in 0 until depNodes.length) {
val node = depNodes.item(i) as Element
val groupId = node.getElementsByTagName("groupId").item(0).textContent
val artifactId = node.getElementsByTagName("artifactId").item(0).textContent
val version = node.getElementsByTagName("version").item(0).textContent
dependencies.add("$groupId:$artifactId:$version")
}
return dependencies
}
// Register a task to import the dependencies of the forage sdk
tasks.register("importForageDependencies") {
doLast {
// adjust this path if you saved the files in a different location
val pomFile = file("libs/forage-android-pos-terminal-sdk/pom-default.xml") // Adjust the path if needed
if (pomFile.exists()) {
val dependencies = parsePomDependencies(pomFile)
dependencies.forEach { dependency ->
println("Adding Forage dependency: $dependency")
dependencies {
add("implementation", dependency)
}
}
} else {
println("POM file not found: $pomFile")
}
}
}
// Run the task before compiling the project
tasks.whenTaskAdded {
if (name.startsWith("compile")) {
dependsOn("importForageDependencies")
}
}- Sync your updated
build.gradle.ktsand you should be all set!- Note: If you change the assets in the
libs/forage-android-pos-terminal-sdkdirectory (like when updating to a new release, for instance), it may not immediately get picked up in Android Studio. Commenting out theimplementation(files("libs/forage-android-pos-terminal-sdk/forage-android-release.aar"))line, syncing (thus temporarily removing the sdk), then un-commenting that line and syncing again should fully pick up any changes.
- Note: If you change the assets in the
Step 2: Add and style a Forage Element
Add a Forage Element.
A Forage Element is a secure, client-side entity that accepts and submits customer input for an EBT transaction.
The Android SDK includes an Element for collecting a card number, ForagePANEditText, and two Element options for collecting the PIN, ForagePINEditText and ForagePinPad. Use the ForagePinPad if your terminal supports a guest facing display or if you expect to display the PIN pad on an external monitor.
You add Elements to your layout file. The following example adds a ForagePINEditText:
<!-- forage_pin_component.xml -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.joinforage.forage.android.pos.ui.element.ForagePINEditText
android:id="@+id/foragePinEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:elementWidth="match_parent"
app:elementHeight="wrap_content"
/>
</androidx.constraintlayout.widget.ConstraintLayout>The below example adds a ForagePANEditText:
<!-- forage_pan_component.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.joinforage.forage.android.pos.ui.element.ForagePANEditText
android:id="@+id/foragePanEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>Finally, the following adds a ForagePinPad:
<!-- forage_pin_pad_component.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.joinforage.forage.android.pos.ui.element.ForagePinPad
android:id="@+id/my_pin_pad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:forage_buttonLayoutMargin="@dimen/keypad_btn_margin"
app:forage_buttonLayoutHeight="@dimen/keypad_btn_height"
app:forage_deleteButtonIcon="@android:drawable/ic_delete"
/>
</androidx.constraintlayout.widget.ConstraintLayout>Style a Forage Element
For a comprehensive list of available customizable styling properties, check out the Android styling reference documentation.
First, add Forage specific theme attributes to your attrs.xml file. The below example adds a ForagePINEditTextStyle:
<!-- attrs.xml -->
<resources>
...
<!-- Theme attribute for the ForagePINEditText on the fragment. -->
<attr name="foragePinEditTextStyle" format="reference" />
...
</resources>Then apply the attribute as a style tag on the Element component file, as in this example snippet:
<!-- forage_pin_component.xml -->
<!-- abridged snippet, style tag only -->
<com.joinforage.forage.android.ui.ForagePINEditText
style="?attr/foragePinEditTextStyle"
/>Next, add an <item> tag for the style in themes.xml, as in the following example:
<!-- themes.xml -->
<resources>
<!-- Base application theme. -->
<style name="Theme.Forageandroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<!-- The ForagePINEditText style -->
<item name="foragePinEditTextStyle">@style/ForagePinEditTextStyle</item>
...
</style>
</resources>Finally, define the style’s properties in styles.xml. The below snippet specifies boxStrokeWidth and boxStrokeColor for the Element:
<!-- styles.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<!-- Style for the ForagePINEditText -->
<style name="ForagePinEditTextStyle">
<item name="boxStrokeWidth">1dp</item>
<item name="boxStrokeColor">@color/mtrl_textinput_default_box_stroke_color</item>
</style>
...
</resources>
Looking to integrate ForagePinPad with a physical card reader?See ForagePinPad & mPOS Integration Guide for a full guide on customizing the PIN pad and handling card swipes with ForageTerminalSDK.
Step 3: Configure a Forage Terminal instance
Set up a request from your front-end to your server to generate a session token.
The below example sends a request from a ViewModel.kt file for a specific EBT transaction, in this case tokenizing an EBT Card number:
// ViewModel.kt
data class TokenResponse(
val token: String,
// ...other fields
)
interface YourServerApi {
@POST("api/session_token")
suspend fun getSessionToken(): TokenResponse
}
class ViewModel : ViewModel() {
private val apiService = Retrofit.Builder()
.baseUrl("your server's base url")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(YourServerApi::class.java)
private val merchantId = BuildConfig.MERCHANT_ID
suspend fun fetchSessionToken(): String {
return withContext(Dispatchers.IO) {
try {
val response = apiService.getSessionToken()
response.token
} catch (e: Exception) {
// Handle the error
// (e.g., log or throw a custom exception)
throw e
}
}
}
}
Hardcoding TokensYou can hardcode a token and Merchant account number during testing, but scrub placeholders before launch. Never expose sensitive tokens on the front-end.
Pass the returned session token, along with the merchant account number, as the ForageConfig in a call to ForageTerminalSDK.init().
ForageConfig in a call to ForageTerminalSDK.init().ForageTerminalSDK.init() creates a Forage Terminal instance. Pass the Context, posTerminalId that uniquely identifies the POS Terminal, and the ForageConfig (which carries the merchantId and sessionToken).
suspend fun init(
context: Context,
posTerminalId: String,
forageConfig: ForageConfig,
ksnDir: File = context.filesDir,
capabilities: TerminalCapabilities = TerminalCapabilities.TapAndInsert
)
Rare Delay WithinitIn rare cases,
initmay take up to 10 seconds. Design your app’s UX to handle this possible delay.
For ForagePANEditText pass the returned session token, along with the merchant account number, in a call to setForageConfig()
ForagePANEditText pass the returned session token, along with the merchant account number, in a call to setForageConfig()setForageConfig() sets foundational settings for a ForagePANEditText, passed as the ForageConfig parameter.
You must call setForageConfig() on an ForagePANEditText Element before invoking any other methods, as in the below example:
val foragePanEditText = root?.findViewById<ForagePANEditText>(R.id.foragePanEditText)
foragePanEditText.setForageConfig(
ForageConfig(
sessionToken = 'sandbox_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...',
merchantId = '123ab45c67'
)
)Step 4: Perform payment operations
After you’ve set up a Forage Android app, added a Forage Element, and configured a Forage Terminal instance, you can execute any of the following payment operations:
- Check a card's balance
- Capture a payment
- Collect a card PIN to defer payment capture to the server
- Capture a payment immediately
- Void a payment
- Refund a payment
- Process a refund immediately
- Defer refund completion to the server
- Void a refund
Continue reading for detailed instructions on each operation.
Check the balance of a card
Elements For Balance CheckUse either a
ForagePINEditTextor aForagePinPadElement to check the balance of an EBT Card.
To check the balance of a card, call checkBalance() on a Forage Terminal SDK instance, passing CheckBalanceParams.
CheckBalanceParams
CheckBalanceParamsdata class CheckBalanceParams(
val forageVaultElement: ForageVaultElement,
val cardholderInteraction: CardholderInteraction
)forageVaultElement(required): A reference to either aForagePINEditTextor aForagePinPadinstance.cardholderInteraction: Represents the information captured by a point-of-sale terminal's card reader during a transaction. This interface is used to handle and transfer card interaction data within the SDK.
Example checkBalance(checkBalanceParams) request
checkBalance(checkBalanceParams) request// BalanceCheckViewModel.kt
class BalanceCheckViewModel : ViewModel() {
fun checkBalance(forageVaultElement: ForagePINEditText) = viewModelScope.launch {
val response = forageTerminalSdk.checkBalance(
CheckBalanceParams(
forageVaultElement = forageVaultElement,
cardholderInteraction = cardholderInteraction,
)
)
when (response) {
is ForageApiResponse.Success -> {
val balance = response.toBalance() as EbtBalance
if (balance is EbtBalance) {
// Unpack balance.snap, balance.cash, balance.paymentMethodRef
}
}
is ForageApiResponse.Failure -> {
// Unpack response.errors
}
}
}
}Capture a payment
Elements For Capturing PaymentUse either a
ForagePINEditTextor aForagePinPadElement to capture a payment.
With the Forage Terminal SDK, you can opt to either defer payment capture to the server, or capture a payment immediately.
Collect a card PIN to defer payment capture to the server
Step 1: Collect a cardholder’s PIN (front-end)
Call the deferPaymentCapture(DeferPaymentCaptureParams) method on a Forage Terminal SDK instance, passing DeferPaymentCaptureParams, to collect a cardholder’s PIN for future server-side payment capture.
DeferPaymentCaptureParams
data class DeferPaymentCaptureParams(
val val forageVaultElement: ForageVaultElement,
val paymentRef: String,
val interaction: CardholderInteraction
)forageVaultElement(required): A reference to either aForagePINEditTextor aForagePinPadinstance.paymentRef(required): A unique string identifier for a previously createdPaymentin Forage's database, returned by the Create aPaymentendpoint.cardholderInteraction: Represents the information captured by a point-of-sale terminal's card reader during a transaction. This interface is used to handle and transfer card interaction data within the SDK.
ExampledeferPaymentCapture(DeferPaymentCaptureParams) request
// DeferPaymentCaptureViewModel.kt
class DeferPaymentCaptureViewModel : ViewModel() {
val snapPaymentRef = "s0alzle0fal"
val merchantId = "<merchant_id>"
val sessionToken = "<session_token>"
fun deferPaymentCapture(forageVaultElement: ForagePINEditText, paymentRef: String) =
viewModelScope.launch {
val response = forageTerminalSdk.deferPaymentCapture(
DeferPaymentCaptureParams(
forageVaultElement = forageVaultElement,
paymentRef = snapPaymentRef,
cardholderInteraction = cardholderInteraction,
)
)
when (response) {
is ForageApiResponse.Success -> {
// there will be no financial affects upon success
// you need to capture from the server to formally
// capture the payment
}
is ForageApiResponse.Failure -> {
// Unpack response.errors
}
}
}
}Step 2: Capture the payment server-side
To complete the payment capture, send a POST from your backend to /api/payments/{payment_ref}/capture_payment/ , as in the below example:
curl --request POST \
--url https://api.sandbox.joinforage.app/api/payments/45e3f12a90/capture_payment/ \
--header 'Authorization: Bearer sandbox_sZawPSUSm9eetx8LrfBbJlzUZS3zWD' \
--header 'Idempotency-Key: 123e4567e8' \
--header 'Merchant-Account: 123ab45c67' \
--header 'accept: application/json'For more information on deferred payment capture, see Forage guide to deferred payment capture.
Capture a payment immediately
To capture a payment immediately, call the capturePayment(CapturePaymentParams) method on a Forage Terminal SDK instance.
CapturePaymentParams
data class CapturePaymentParams(
val forageVaultElement: ForageVaultElement<ElementState>,
val paymentRef: String,
val interaction: CardholderInteraction
)forageVaultElement(required): A reference to either aForagePINEditTextor aForagePinPadinstance.paymentRef(required): A unique string identifier for a previously createdPaymentin Forage's database, returned by the Create aPaymentendpoint.CardholderInteraction: Represents the information captured by a point-of-sale terminal's card reader during a transaction. This interface is used to handle and transfer card interaction data within the SDK.
ExamplecapturePayment(CapturePaymentParams) request
// PaymentCaptureViewModel.kt
class PaymentCaptureViewModel : ViewModel() {
val snapPaymentRef = "s0alzle0fal"
val merchantId = "<merchant_id>"
val sessionToken = "<session_token>"
fun capturePayment(forageVaultElement: ForagePINEditText, paymentRef: String) =
viewModelScope.launch {
val response = forageTerminalSdk.capturePayment(
CapturePaymentParams(
foragePinEditText = foragePinEditText,
paymentRef = snapPaymentRef,
cardholderInteraction = cardholderInteraction,
)
)
when (response) {
is ForageApiResponse.Success -> {
// handle successful capture
val payment = response.toPayment()
// Unpack payment.ref, payment.amount, ...
}
is ForageApiResponse.Failure -> {
val error = response.errors[0]
// handle Insufficient Funds error
if (error.code == "ebt_error_51") {
val details = error.details as ForageErrorDetails.EbtError51Details
val (snapBalance, cashBalance) = details
// do something with balances ...
}
}
}
}
}Void a payment
Server-side
To void a payment, send a request from your backend to /api/payments/{payment_ref}/void/, as in the following example:
curl --request POST \
--url https://api.sandbox.joinforage.app/api/payments/45e3f12a90/void/ \
--header 'Authorization: Bearer sandbox_sZawPSUSm9eetx8LrfBbJlzUZS3zWD' \
--header 'Idempotency-Key: 123e4567e8' \
--header 'Merchant-Account: 123ab45c67' \
--header 'accept: application/json'For more information on voiding a payment, see Void a Payment reference docs.
Refund a payment
Refund A Payment ElementsUse either a
ForagePINEditTextor aForagePinPadElement to refund a payment.
Whether you process a refund immediately or defer refund completion to the server, if a refund attempt fails then prompt the customer to repeat the request.
Process a refund immediately
To refund an EBT POS payment immediately, pass RefundPaymentParams to the refundPayment() method.
data class RefundPaymentParams(
val forageVaultElement: ForageVaultElement<ElementState>,
val paymentRef: String,
val amount: Float,
val reason: String,
val metadata: Map<String, String>? = null,
val interaction: CardholderInteraction
)
suspend fun refundPayment(
params: RefundPaymentParams
): ForageApiResponse<String>RefundPaymentParams
RefundPaymentParamsforageVaultElement(required): A reference to either aForagePINEditTextor aForagePinPadinstance.paymentRef: A unique string identifier for a previously createdPaymentin Forage's database, returned by the Create a Payment endpoint.amount: A positive decimal number that represents how much of the original payment to refund in USD. Precision to the penny is supported. The minimum amount that can be refunded is0.01.reason: A string that describes why the payment is to be refunded.metadata: A required object containing merchant-defined key-value pairs to provide additional context for the payment. Use this field to store internal identifiers (for example, order IDs, user IDs) rather than raw data. If no additional information is available, pass an empty object ({}).Avoid PII In MetadataDo not include personally identifiable information (PII) in metadata, such as names, emails, or payment details.
cardholderInteraction: Represents the information captured by a point-of-sale terminal's card reader during a transaction. This interface is used to handle and transfer card interaction data within the SDK.
Example refundPayment request
refundPayment request// PosRefundViewModel.kt
class PosRefundViewModel : ViewModel() {
var paymentRef: String = ""
var amount: Float = 0.0
var reason: String = ""
var metadata: HashMap? = null
fun refundPayment(forageVaultElement: ForagePINEditText) = viewModelScope.launch {
val refundParams = RefundPaymentParams(
forageVaultElement,
paymentRef,
amount,
reason,
metadata,
)
val response = forageTerminalSdk.refundPayment(refundParams)
when (response) {
is ForageApiResponse.Success -> {
// do something with response.data
}
is ForageApiResponse.Failure -> {
// do something with response.errors
}
}
}
}
Defer refund completion to the server
Step 1: Collect a customer’s EBT Card PIN (front-end)
After initializing the Terminal SDK and creating a Forage Element, pass DeferPaymentRefundParams to the deferPaymentRefund() method.
data class DeferPaymentCaptureParams(
val forageVaultElement: ForageVaultElement<ElementState>,
val paymentRef: String,
val interaction: CardholderInteraction
)
suspend fun deferPaymentRefund(
params: DeferPaymentRefundParams
): ForageApiResponse<String>DeferPaymentRefundParams
forageVaultElement(required): A reference to either aForagePINEditTextor aForagePinPadinstance.paymentRef: A unique string identifier for the previously createdPaymentin Forage's database, returned by the Create aPaymentendpoint when the payment was first created.cardholderInteraction: Represents the information captured by a point-of-sale terminal's card reader during a transaction. This interface is used to handle and transfer card interaction data within the SDK.
ExampledeferPaymentRefund request:
// PosDeferPaymentRefundViewModel.kt
class PosDeferPaymentRefundViewModel : ViewModel() {
var paymentRef: String = ""
fun deferPaymentRefund(forageVaultElement: ForagePINEditText) = viewModelScope.launch {
val deferPaymentRefundParams = DeferPaymentRefundParams(
forageVaultElement,
paymentRef
)
val response = forageTerminalSdk.deferPaymentRefund(deferPaymentRefundParams)
when (response) {
is ForageApiResponse.Success -> {
// do something with response.data
}
is ForageApiResponse.Failure -> {
// do something with response.errors
}
}
}See the Terminal SDK docs for details on using deferPaymentRefund.
Step 2: Complete the Refund (server-side)
Send a POST to /payments/{payment_ref}/refunds/ to complete the refund. Pass the amount, reason, provider_terminal_id, and metadata in the body of the 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-id>' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"amount": "<amount>",
"reason": "<reason>",
"pos_terminal": {
"provider_terminal_id": "<provider_terminal_id>"
},
"metadata": {}
}
'See the Payments API docs for instructions on creating a PaymentRefund.
Refund And Receipt Class ExamplesSee example Refund and Receipt classes. The response object structure is the same across all refund methods.
Void a refund
Server-side
To void a refund, send a request from your backend to /api/payments/{payment_ref}/refunds/{refund_ref}/void/, as in the following example:
curl --request POST \
--url https://api.sandbox.joinforage.app/api/payments/45e3f12a90/refunds/b873fe62dc/void/ \
--header 'Authorization: Bearer sandbox_sZawPSUSm9eetx8LrfBbJlzUZS3zWD' \
--header 'Idempotency-Key: 123e4567e8' \
--header 'Merchant-Account: 123ab45c67' \
--header 'accept: application/json'See the payment API reference docs for information on how to void a payment refund.
Updated 5 days ago
