HomeGuidesReference
Log In

Online-only Android Quickstart

Use the Forage Android SDK to accept online-only EBT 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

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

  1. 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
  1. 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.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>
  1. Style a Forage Element.

Whether you’re styling a ForagePINEditText or a ForagePANEditText 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.

  1. 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 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 -> {
                // ...Handle success response...
                // To offer a wallet, save paymentMethod.ref to your database
            }
            is ForageApiResponse.Failure -> {
                // ...Handle error response...
            }
        } 
    }
}

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 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,
    private val moshi: Moshi
) : 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 -> {
                // Handle success response...
            }
            is ForageApiResponse.Failure -> {
                // Handle error response...
            }
        }
    }
}

Capture a payment

Use the ForagePINEditText Element to capture a payment.

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 = `mid/${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 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 -> {
                    // Handle success response...
                }
                is ForageApiResponse.Failure -> {
                    // Handle error response...
                }
            }
        }

    fun captureCashAmount(context: Context, foragePinEditText: ForagePINEditText) =
        viewModelScope.launch {
            val response = ForageSDK().capturePayment(
                CapturePaymentParams(
                    foragePinEditText = foragePinEditText,
                    paymentRef = cashPaymentRef
                )
            )

            when (response) {
                is ForageApiResponse.Success -> {
                    // Handle success response...
                }
                is ForageApiResponse.Failure -> {
                    // Handle error response...
                }
            }
        }
}