Android Quickstart
Use the Forage Android SDK to accept online-only EBT SNAP payments in your mobile app.
In this guide, you’ll learn how to:
- Set up a Forage Android app
- Add and style a Forage Element
- Configure a Forage instance
- Perform payment operations
This guide covers online-only EBT SNAP payments.
Refer to the POS Terminal Android Quickstart for instructions on accepting in-store payments.
Prerequisites
- Sign up for a Forage account (contact us if you don’t yet have one)
- Install Android API Level 21 or higher
- Install the following dependencies to copy/paste code snippets and run the sample app in the GitHub repository:
Resources
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.
The following code snippets assume that environment variables are stored in a
.env
Edit the relevant lines of code to match your token management preferences or dev environment. Just be careful to never expose sensitive data like authentication tokens in client-side requests.
// 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 = {
Accept: 'application/json',
Authorization: `Bearer ${authenticationToken}`,
'Merchant-Account': merchantId,
'Content-Type': 'application/x-www-form-urlencoded'
}
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 = {
'Accept' => 'application/json',
'Authorization' => "Bearer #{authentication_token}",
'Merchant-Account' => merchant_id,
'Content-Type' => 'application/x-www-form-urlencoded',
}
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
end
2. Install the Android SDK.
Add the forage-android
dependency to your build.gradle
module. Replace x.x.x
with the Forage version of your choice:
// build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
// ...
}
dependencies {
// ...
// Forage Android SDK
implementation 'com.joinforage:forage-android:x.x.x'
// ...
}
Add maven("https://jitpack.io")
inside therepositories
block insettings.gradle.kts
:
// settings.gradle.kts
pluginManagement {
// ...
}
dependencyResolutionManagement {
// …
repositories {
// ...
maven("https://jitpack.io")
}
}
// ...
Step 2: Add and style a Forage Element
1. Add a Forage Element.
A Forage Element is a secure, client-side entity that accepts and submits customer input for a transaction.
The Android SDK includes an Element for collecting a card number, ForagePANEditText
, and an Element to collect a card PIN, ForagePINEditText
.
Add Elements to a file in your app’s /layout/
directory. The below example adds 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.ui.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>
The following example adds ForagePINEditText
:
<!-- forage_pin_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.ui.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"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
2. Style a Forage Element.
Whether you’re styling a
ForagePINEditText
or aForagePANEditText
Element, the steps are the same. The below examples style a PAN Element.
For a comprehensive list of available customizable styling properties, check out the Android styling reference documentation.
First, add a theme attribute for the Element to your attrs.xml
. This example adds an attribute for the PAN Element called tokenizeForagePANEditTextStyle
:
<!-- attrs.xml -->
<resources>
...
<!-- Theme attribute for the ForagePANEditText on the tokenize fragment. -->
<attr name="tokenizeForagePANEditTextStyle" format="reference" />
...
</resources>
Then apply the attribute as a style
tag on the Element component file, as in this example snippet:
<!-- forage_pan_component.xml -->
<!-- abridged snippet, style tag only -->
<com.joinforage.forage.android.ui.ForagePANEditText
style="?attr/tokenizeForagePANEditTextStyle"
/>
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">
...
<!-- The ForagePANEditText style -->
<item name="tokenizeForagePANEditTextStyle">@style/TokenizeForagePANEditTextStyle</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 ForagePANEditText -->
<style name="TokenizeForagePANEditTextStyle">
<item name="panBoxStrokeColor">@color/pan_box_stroke_color</item>
<item name="panBoxStrokeWidth">1dp</item>
</style>
...
</resources>
Step 3: Configure a Forage instance
1. 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:
// TokenizeViewModel.kt
data class TokenResponse(
val token: String,
// ...other fields
)
interface YourServerApi {
@POST("api/session_token")
suspend fun getSessionToken(): TokenResponse
}
class TokenizeViewModel : 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
}
}
}
}
During testing and development, you can opt to hardcode a session token and the merchant’s account number. Just remember to scrub placeholder values before going live in production. To keep your app secure, never expose sensitive tokens on the front-end.
2. Pass the returned session token, along with the merchant account number, in a call to setForageConfig()
.
setForageConfig()
.The following snippet calls setForageConfig()
from a Fragment.kt
file for a specific EBT transaction, in this case a tokenizing an EBT Card Number:
// TokenizeFragment.kt
@AndroidEntryPoint
class TokenizeFragment : Fragment() {
private val viewModel: TokenizeViewModel by viewModels()
private var _binding: TokenizeFragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Establish bindings to ForagePANEditText
_binding = TokenizeFragmentBinding.inflate(inflater, container, false)
val foragePanEditText: ForagePANEditText = binding.tokenizeForagePanEditText
// Fetch sessionToken from server
val sessionToken = viewModel.fetchSessionToken()
// Immediately call setForageConfig() on the binding
foragePanEditText.setForageConfig(
ForageConfig(
merchantId = viewModel.merchantId,
sessionToken = sessionToken
)
)
// Then freely call other methods on ForagePANEditText binding
foragePanEditText.requestFocus()
}
}
Step 4: Perform payment operations
Tokenize and store a payment method
To tokenize an EBT Card number, use the
ForagePANEditText
Element.
Building on the example code in Steps 1-3, in TokenizeViewModel.kt
call the tokenizeEBTCard()
Forage SDK method. Pass a call to TokenizeEBTCardParams()
as the parameter, as in the following example:
// TokenizeViewModel.kt
class TokenizeViewModel : ViewModel() {
// ...Set up retrofit...
// LiveData for observing the result of the tokenization process
val tokenizationResult = MutableLiveData<Result<PaymentMethod>>()
// ...Function to fetch the session token...
// Tokenize the EBT card
fun tokenizeEBTCard(foragePanEditText: ForagePANEditText) = viewModelScope.launch {
val response = ForageSDK().tokenizeEBTCard(
TokenizeEBTCardParams(
foragePanEditText = foragePanEditText,
reusable = true,
// NOTE: The following line is for testing purposes only
// It should not be used in production
// Replace this line with a real hashed customer ID value
customerId = UUID.randomUUID().toString(),
)
)
when (response) {
is ForageApiResponse.Success -> {
val paymentMethod = response.toPaymentMethod()
// Unpack paymentMethod.ref, paymentMethod.card, etc.
val card = paymentMethod.card
// Unpack card.last4, ...
if (card is EbtCard) {
// Unpack ebtCard.usState, ...
}
// To offer a wallet, save paymentMethod.ref to your database
}
is ForageApiResponse.Failure -> {
// Unpack response.errors
}
}
}
}
Check the balance of a payment method
To check the balance of an EBT Card, use the
ForagePINEditText
Element.
FNS requirements for balance inquiries
FNS prohibits balance inquiries on sites and apps that offer guest checkout. Skip this transaction if your customers can opt for guest checkout.
If guest checkout is not an option, then it's up to you whether or not to add a balance inquiry feature. No FNS regulations apply.
First, repeat steps 2-3 for a new ForagePINEditText
Element, in summary:
- Add the
ForagePINEditText
in the/layout/
directory if you haven’t already - Style the Element
- In a
ViewModel.kt
for the Balance Check operation, for exampleBalanceCheckViewModel.kt
, write a request to generate a Forage session token - In a
Fragment.kt
for the Balance Check, for exampleBalanceCheckFragment.kt
, call the View Model and pass the returned session token tosetForageConfig()
Then, from BalanceCheckViewModel.kt
, call the checkBalance
Forage SDK method. Pass a call to CheckBalanceParams()
as the parameter, as in the following example:
// BalanceCheckViewModel.kt
@HiltViewModel
class BalanceCheckViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val args = CheckBalanceFragmentArgs.fromSavedStateHandle(savedStateHandle)
// Internal so that BalanceCheckFragment can access these values
val paymentMethodRef = args.paymentMethodRef
fun checkBalance(foragePinEditText: ForagePINEditText) = viewModelScope.launch {
val response = ForageSDK().checkBalance(
CheckBalanceParams(
foragePinEditText = foragePinEditText,
paymentMethodRef = paymentMethodRef
)
)
when (response) {
is ForageApiResponse.Success -> {
val balance = response.toBalance() as EbtBalance
if (balance is EbtBalance) {
// Unpack balance.snap, balance.cash
}
}
is ForageApiResponse.Failure -> {
// Unpack response.errors
}
}
}
}
Capture a payment
Use the
ForagePINEditText
Element to capture a payment.
Deferred payment capture
If you plan to defer payment capture to the server, then start with the deferred payment capture guide instead.
To capture a payment, you first need to create a Forage Payment
object.
Set up an endpoint on your server that creates a Payment
. A POST
to the Forage /payments/
endpoint creates a Forage Payment object.
The following examples assume that environment variables are stored in a
.env
Edit the relevant lines of code if that doesn’t match your token management preferences or dev environment. Just be careful to never expose sensitive data like authentication tokens in client-side requests.
// app.js
const express = require('express')
const bodyParser = require('body-parser')
const axios = require('axios')
require('dotenv').config();
const app = express()
const port = 5000
app.use(bodyParser.json())
app.post('/api/create_payment', async (req, res) => {
try {
const { snapAmount } = req.body
const authenticationToken = process.env.AUTHENTICATION_TOKEN // Replace with your authentication token
const merchantId = process.env.MERCHANT_ID // Replace with your Forage merchant ID
const paymentPayload = {
amount: snapAmount,
funding_type: 'ebt_snap',
payment_method: paymentMethodRef,
description: 'Test EBT SNAP payment',
metadata: {},
is_delivery: true,
delivery_address: {
country: 'US',
city: 'Los Angeles',
line1: '4121 Santa Monica Blvd',
line2: 'Unit 513',
zipcode: '90029',
state: 'CA'
}
}
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${authenticationToken}`,
'Merchant-Account': merchantId, // replace with your Forage merchant ID
'Content-Type': 'application/json',
'IDEMPOTENCY-KEY': Math.random() * 10000 // Udpate in production: https://docs.joinforage.app/reference/idempotency
}
const response = await axios.post(
'https://api.sandbox.joinforage.app/api/payments/',
paymentPayload,
{ headers }
)
const foragePayment = response.data
console.log('Forage Payment created:', foragePayment)
res.json({ success: true, payment: foragePayment })
} catch (error) {
console.error('Error occurred:', error.message)
res.status(500).json({ success: false, 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 PaymentsController < Sinatra::Base
post '/create_payment' do
begin
authentication_token = ENV['AUTHENTICATION_TOKEN'] # Replace with your authentication token
merchant_id = 'mid/#{ENV['MERCHANT_ID']}' # Replace with your Forage merchant ID
snap_amount = params[:snapAmount]
payment_payload = {
amount: snap_amount,
funding_type: 'ebt_snap',
payment_method: payment_method,
description: 'Test EBT SNAP payment',
metadata: {},
is_delivery: true,
delivery_address: {
country: 'US',
city: 'Los Angeles',
line1: '4121 Santa Monica Blvd',
line2: 'Unit 513',
zipcode: '90029',
state: 'CA'
}
}
api_url = 'https://api.sandbox.joinforage.app/api/payments/'
uri = URI(api_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
headers = {
'Accept' => 'application/json',
'Authorization' => "Bearer #{authentication_token}",
'Merchant-Account' => merchant_id
'Content-Type' => 'application/json',
'IDEMPOTENCY-KEY' => rand(10000).to_s # Update me in production: https://docs.joinforage.app/reference/idempotency
}
response = http.post(uri.path, payment_payload.to_json, headers)
if response.is_a?(Net::HTTPSuccess)
forage_payment = JSON.parse(response.body)
puts 'Forage Payment created:', forage_payment
content_type :json
{ success: true, payment: forage_payment }.to_json
else
status :internal_server_error
{ success: false, error: 'Something went wrong' }.to_json
end
rescue StandardError => e
status :internal_server_error
{ success: false, error: 'Something went wrong' }.to_json
end
end
end
Next, set up the front-end code in your app that collects the customer input to be to passed to the endpoint.
Create a Fragment.kt
and connect it to a ViewModel
. This example CreatePaymentFragment.kt
references a CreatePaymentViewModel
:
// CreatePaymentFragment.kt
@AndroidEntryPoint
class CreatePaymentFragment : Fragment() {
private val viewModel: CreatePaymentViewModel by viewModels()
private var _binding: FragmentCreatePaymentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCreatePaymentBinding.inflate(inflater, container, false)
val root: View = binding.root
// Pass customer input + PaymentMethod ref to your server
binding.submitSnapAmount.setOnClickListener {
viewModel.createPayment(
snapAmount = getSnapAmount(),
paymentMethodRef = viewModel.paymentMethodRef,
)
it.context.hideKeyboard(it)
}
// Connect customer inputs to the viewModel
viewModel.snapPaymentResult.observe(viewLifecycleOwner) {
it?.let {
binding.snapResponse.text = """
Amount: ${it.amount}
Funding Type: ${it.fundingType}
$it
""".trimIndent()
binding.nextButton.visibility = View.VISIBLE
}
}
// When a customer clicks "Next", send their input to the ViewModel
binding.nextButton.setOnClickListener {
findNavController().navigate(
CreatePaymentFragmentDirections.actionCreatePaymentFragmentToCapturePaymentFragment(
sessionToken = viewModel.sessionToken,
merchantId = viewModel.merchantId,
paymentMethodRef = viewModel.paymentMethodRef,
snapAmount = getSnapAmount(),
snapPaymentRef = viewModel.snapPaymentResult.value?.ref.orEmpty()
)
)
}
return root
}
// Listen for customer inputs
private fun getSnapAmount(): Long {
return try {
binding.snapAmountEditText.text.toString().toLong()
} catch (e: NumberFormatException) {
0
}
}
}
The CreatePaymentViewModel
sends the customer input data to Forage to create the Payment
.
// CreatePaymentViewModel.kt
class CreatePaymentViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val args = CreatePaymentFragmentArgs.fromSavedStateHandle(savedStateHandle)
val merchantId = args.merchantId
val sessionToken = args.sessionToken
val paymentMethodRef = args.paymentMethodRef
private val apiService = Retrofit.Builder()
.baseUrl("/create_payment")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
private val _snapPaymentResult = MutableLiveData<PaymentResponse?>().apply {
value = null
}
val snapPaymentResult: LiveData<PaymentResponse?> = _snapPaymentResult
suspend fun createPayment(
sessionToken: String,
merchantId: String,
amount: Long,
fundingType: String,
paymentMethod: String,
description: String,
deliveryAddress: String,
isDelivery: Boolean
): String {
return withContext(Dispatchers.IO) {
try {
val response = apiService.createPayment(
sessionToken = sessionToken,
merchantId = merchantId,
amount = amount,
fundingType = "ebt_snap",
paymentMethod = paymentMethod,
description = "desc",
deliveryAddress = DEV_ADDRESS,
isDelivery = isDelivery
)
_snapPaymentResult.value = response
} catch (e: Exception) {
// Handle the error (e.g., log or throw a custom exception)
throw e
}
}
}
// Replace in testing and production
companion object {
private val DEV_ADDRESS = Address(
city = "San Francisco",
country = "United States",
line1 = "Street",
line2 = "Number",
state = "CA",
zipcode = "12345"
)
}
}
With the code to create a payment established, you’re ready to set up capture. First, repeat steps 2-3 for a ForagePINEditText
Element, in summary:
- Add the
ForagePINEditText
Element in the/layout/
directory if you haven’t already - Style the Element
- In a
ViewModel.kt
for the Payment Capture operation, for examplePaymentCaptureViewModel.kt
, write a request to generate a Forage session token - In a
Fragment.kt
for the PaymentCapture, for examplePaymentCaptureFragment.kt
, call the View Model and pass the returned session token in a call tosetForageConfig
Then, from PaymentCaptureViewModel.kt
, call the capturePayment
Forage SDK method, passing a call to CapturePaymentParams
as the parameter, as in the example below:
// PaymentCaptureViewModel.kt
@HiltViewModel
class CapturePaymentViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
) : BaseViewModel() {
private val TAG = CapturePaymentViewModel::class.java.simpleName
private val apiService = Retrofit.Builder()
.baseUrl("/session_token")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
private val args = CapturePaymentFragmentArgs.fromSavedStateHandle(savedStateHandle)
private val snapAmount = args.snapAmount
private val cashAmount = args.cashAmount
internal val merchantId = args.merchantId
internal val sessionToken = args.sessionToken
private val snapPaymentRef = args.snapPaymentRef
private val _uiState = MutableLiveData(
FlowCapturePaymentUIState(
snapAmount = snapAmount,
cashAmount = cashAmount,
snapPaymentRef = snapPaymentRef,
cashPaymentRef = cashPaymentRef
)
)
val uiState: LiveData<FlowCapturePaymentUIState> = _uiState
fun captureSnapAmount(context: Context, foragePinEditText: ForagePINEditText) =
viewModelScope.launch {
val response = ForageSDK().capturePayment(
CapturePaymentParams(
foragePinEditText = foragePinEditText,
paymentRef = snapPaymentRef
)
)
when (response) {
is ForageApiResponse.Success -> {
val payment = response.toPayment()
// Unpack payment.ref, payment.amount, payment.receipt, etc.
}
is ForageApiResponse.Failure -> {
// Unpack response.errors
}
}
}
fun captureCashAmount(context: Context, foragePinEditText: ForagePINEditText) =
viewModelScope.launch {
val response = ForageSDK().capturePayment(
CapturePaymentParams(
foragePinEditText = foragePinEditText,
paymentRef = cashPaymentRef
)
)
when (response) {
is ForageApiResponse.Success -> {
val payment = response.toPayment()
// Unpack payment.ref, payment.amount, payment.receipt, etc.
}
is ForageApiResponse.Failure -> {
// Unpack response.errors
}
}
}
}
Updated 5 months ago