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
Online-Only Payments Covered
This guide covers EBT SNAP payments for online purchases only. For in-store payments, see the POS Terminal Android Quickstart.
Prerequisites
- Sign up for a Forage account (contact us if you don’t yet have one)
- Install Android API Level 23 or newer.
- Install the following dependencies to copy/paste code snippets and run the sample app in the GitHub repository:
For more information, check out:
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 Warning
The code examples assume you’re using an
.env
file. Update them as needed, but never expose sensitive data like 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 = {
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.ecom.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>
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.ecom.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"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
2. Style a Forage Element.
Styling PIN And PAN Elements
Whether you’re styling a
ForagePINEditText
or aForagePANEditText
Element, the steps are the same. The below examples style a PAN Element.
Android Styling Reference
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.ecom.ui.element.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
}
}
}
}
Token Use During Testing
You can hardcode a session token and account number while testing, but scrub placeholder values before going live. 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 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
How To Tokenize Card Numbers
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
How To Check EBT Card Balance
To check the balance of an EBT Card, use the
ForagePINEditText
Element.
FNS Balance Inquiry Rules
FNS prohibits balance checks on sites with guest checkout. If guest checkout isn’t offered, adding a balance check is optional.
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
How To Capture A Payment
Use the
ForagePINEditText
Element to capture a payment.
Server-Side Payment Capture
If you plan to defer capture to the server, 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.
Protect Environment Variables
These examples assume you’re using a
.env
file. Adjust as needed, but never expose 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 established code to create a payment, 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 6 days ago