HomeGuidesReference
Log In
Guides

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:

  1. Set up a Forage Android app
  2. Add and style a Forage Element
  3. Configure a Forage instance
  4. Perform payment operations
    1. Tokenize and store a payment method
    2. Check the balance of a payment method
    3. Capture a payment

📘

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 therepositoriesblock 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 a ForagePANEditText 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().

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 example BalanceCheckViewModel.kt, write a request to generate a Forage session token
  • In a Fragment.kt for the Balance Check, for example BalanceCheckFragment.kt, call the View Model and pass the returned session token to setForageConfig()

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 example PaymentCaptureViewModel.kt, write a request to generate a Forage session token
  • In a Fragment.kt for the PaymentCapture, for example PaymentCaptureFragment.kt, call the View Model and pass the returned session token in a call to setForageConfig

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