HomeGuidesReference
Log In

POS Terminal Android Quickstart

Use the Forage Terminal SDK to accept EBT SNAP and other payment methods via a POS system.

This guide explains how to get a foundational Forage POS Terminal Android app up and running. It also introduces the SDK's core payment operations. After reading the guide, you'll know how to complete the following steps:

  1. Set up a Forage Android app
  2. Add and style a Forage Element
  3. Configure a Forage Terminal instance
  4. Execute payment operations

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/pasted code snippets and run the sample-app:

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 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 axios = require('axios')
const express = require('express')
const bodyParser = require('body-parser')
require('dotenv').config();

const app = express()
const port = 5000

// Middleware to parse the request body
app.use(bodyParser.urlencoded({ extended: true }))

app.post('/api/session_token', async (req, res) => {
  try {
    const apiUrl = 'https://api.sandbox.joinforage.app/api/session_token/'
    // Replace the below  with your authentication token
    const authenticationToken = process.env.AUTHENTICATION_TOKEN
    // Replace the below  with your Forage merchant ID
    const merchantId = process.env.MERCHANT_ID

    const headers = {
      Authorization: `Bearer ${authenticationToken}`,
      'Merchant-Account': merchantId,
      'Content-Type': 'application/json'
    }

    const response = await axios.post(apiUrl, null, { headers })

    res.json(response.data)
  } catch (error) {
    res
      .status(error.response ? error.response.status : 500)
      .json({ error: 'Something went wrong' })
  }
})

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`)
})
# app.rb

require 'sinatra'
require 'net/http'
require 'json'
require 'dotenv/load'

class ApiController < Sinatra::Base
  post '/session_token' do
    begin
      api_url = 'https://api.sandbox.joinforage.app/api/session_token/'
      # Replace the below with your authentication token
      authentication_token = ENV['AUTHENTICATION_TOKEN']
      # Replace the below with your Forage merchant ID
      merchant_id = ENV['MERCHANT_ID']

      uri = URI(api_url)
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = true

      headers = {
        'Authorization' => "Bearer #{authentication_token}",
        'Merchant-Account' => merchant_id,
        'Content-Type' => 'application/json',
      }

      response = http.post(uri.path, nil, headers)

      if response.is_a?(Net::HTTPSuccess)
        json_response = JSON.parse(response.body)
        content_type :json
        json_response.to_json
      else
        status response.code
        { error: 'Something went wrong' }.to_json
      end
    rescue StandardError => e
      status e.respond_to?(:status) ? e.status : 500
      { error: 'Something went wrong' }.to_json
    end
  end
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 the repositories block in settings.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 an EBT transaction.

The Android SDK includes an Element for collecting an EBT Card Number, ForagePANEditText, and an Element to collect an EBT Card PIN, ForagePINEditText.

You add Elements to your layout file. 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"
        app:elementWidth="match_parent"
        app:elementHeight="wrap_content"
    />

</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 PIN Element.

📘

For a comprehensive list of available customizable styling properties, check out the Android styling reference documentation.

First, add Forage specific theme attributes to your attrs.xml file. The below example adds a tokenizeForagePINEditTextStyle:

<!-- attrs.xml -->

<resources>
    ...
    <!-- Theme attribute for the ForagePINEditText on the tokenize fragment. -->
    <attr name="foragePinEditTextStyle" format="reference" />
    ...
</resources>

Then apply the attribute as a style tag on the Element component file, as in this example snippet:

<!-- forage_pin_component.xml -->

<!-- abridged snippet, style tag only -->
<com.joinforage.forage.android.ui.ForagePINEditText
    style="?attr/foragePinEditTextStyle"
/>

Next, add an <item> tag for the style in themes.xml, as in the following example:

<!-- themes.xml -->

<resources>
    <!-- Base application theme. -->
    <style name="Theme.Forageandroid" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        ...
        <!-- The ForagePINEditText style -->
        <item name="foragePinEditTextStyle">@style/ForagePinEditTextStyle</item>
        ...
    </style>
</resources>

Finally, define the style’s properties in styles.xml. The below snippet specifies boxStrokeWidth and boxStrokeColor for the Element:

<!-- styles.xml -->

<?xml version="1.0" encoding="utf-8"?>
<resources>
    ...
    <!-- Style for the ForagePINEditText -->
    <style name="ForagePinEditTextStyle">
        <item name="boxStrokeWidth">1dp</item>
        <item name="boxStrokeColor">@color/mtrl_textinput_default_box_stroke_color</item>
    </style>
    ...
</resources>

Step 3: Configure a Forage Terminal 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 an authentication token and the merchant’s account number instead. 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 setPosForageConfig().

setPosForageConfig() sets foundational settings for a Forage Element, passed as the PosForageConfig parameter.

You must call setPosForageConfig() on an Element before invoking any other methods, as in the below example for a ForagePINEditText Element:

val foragePinEditText = root?.findViewById<ForagePINEditText>(R.id.foragePinEditText)
foragePinEditText.setPosForageConfig(
    PosForageConfig(
        sessionToken = 'sandbox_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...',
        merchantId = 'mid/123ab45c67'
    )
)

Example: Call setPosForageConfig() in a Fragment

The following snippet calls setPosForageConfig() after a Fragment for a specific payment operation, in this case a refund, is created:

// PosRefundFragment.kt

@AndroidEntryPoint
class PosRefundFragment : Fragment() {
    private val viewModel: PosRefundViewModel by viewModels()
    private var _binding: PosRefundFragmentBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // establish bindings to ForagePANEditText
        _binding = PosRefundFragmentBinding.inflate(inflater, container, false)
        val foragePinEditText: ForagePINEditText = binding.posRefundForagePinEditText

        // immediately call setPosForageConfig() on the binding
        foragePinEditText.setPosForageConfig(viewModel.forageConfig)

        // then freely call other methods on ForagePINEditText binding
        foragePinEditText.requestFocus()
    }
}

  1. Call ForageTerminalSDK.init() to initialize the Forage Terminal SDK.

ForageTerminalSDK.init() creates a Forage Terminal instance. Pass the Context , posTerminalId that uniquely identifies the POS Terminal, the merchantId, and the sessionToken.

try {
    val forageTerminalSdk =
        ForageTerminalSDK.init(
            context = getApplicationContext(),
            posTerminalId = "<id-that-uniquely-identifies-the-pos-terminal>",
            posForageConfig = PosForageConfig(
                merchantId = "mid/123ab45c67",
                sessionToken = "sandbox_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
            )
        )

    // Use the forageTerminalSdk to call other methods
    // (e.g. tokenizeCard, checkBalance, deferPaymentCapture, etc.)
} catch (e: Exception) {
    // handle initialization error
}

🚧

In very few cases,init can take up to 10 seconds to run.

Although it rarely takes this long, account for the potential delay in your app’s UX design.

Step 4: Perform payment operations

After you’ve set up a Forage Android app, added a Forage Element, and configured a Forage Terminal instance, you can execute any of the following payment operations:

Continue reading for detailed instructions on each operation.

Tokenize a card

With a Forage Terminal POS SDK integration, you can tokenize a card either via a ForagePANEditText Element or a magnetic card swipe.

Tokenize a card via a ForagePANEditText Element

To tokenize a card via a a ForagePANEditText Element, call tokenizeCard() on a ForageTerminalSDK instance, passing a reference to the Element in the method call.

Example tokenizeCard() request
// TokenizeViewModel.kt
@HiltViewModel
class TokenizeViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val moshi: Moshi
) : ViewModel() {
    private val args = TokenizeFragmentArgs.fromSavedStateHandle(savedStateHandle)

    // internal so that TokenizeFragment can access these values
    val merchantId = args.merchantId
    val sessionToken = args.sessionToken

    fun tokenizeCard(foragePanEditText: ForagePANEditText) = viewModelScope.launch {
        _isLoading.value = true

        val response = forageTerminalSdk.tokenizeCard(
          foragePanEditText = foragePanEditText,
          reusable = true
        )

        when (response) {
            is ForageApiResponse.Success -> {
                val adapter: JsonAdapter<PaymentMethod> = moshi.adapter(PaymentMethod::class.java)
                val paymentMethod = adapter.fromJson(response.data)

                // (optional) do something with the ref
                saveToPaymentMethodRefToMyAPI(paymentMethod.ref)
            }
            is ForageApiResponse.Failure -> {
                _error.value = response.message
            }
        }

        _isLoading.value = false
    }
}

Tokenize a card via a magnetic swipe

To tokenize a card from a magnetic card swipe on a POS Terminal, call tokenizeCard() on a ForageTerminalSDK instance, passing PosTokenizeCardParams.

PosTokenizeCardParams
data class PosTokenizeCardParams(
    val posForageConfig: PosForageConfig,
    val track2Data: String,
    val reusable: Boolean = true
)
  • posForageConfig (required): The configuration details required to authenticate with the Forage API.
    • merchantId: A unique Merchant ID that Forage provides during onboarding onboarding preceded by "mid/". For example, mid/123ab45c67. The Merchant ID can be found in the Forage Sandbox or Production Dashboard.
    • sessionToken: A short-lived token that authenticates front-end requests to Forage. To create one, send a server-side request from your backend to the /session_token/ endpoint.
  • track2data (required): The information encoded on Track 2 of the EBT Card’s magnetic stripe, excluding the start and stop sentinels and any LRC characters.
  • reusable: An optional boolean value indicating whether the same card can be used to make multiple payments, set to true by default.

Example tokenizeCard(PosTokenizeCardParams) request

// TokenizePosViewModel.kt
@HiltViewModel
class TokenizePosViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val moshi: Moshi
) : ViewModel() {
    private val args = TokenizePosFragmentArgs.fromSavedStateHandle(savedStateHandle)

    // internal so that TokenizePosFragment can access these values
    val merchantId = args.merchantId
    val sessionToken = args.sessionToken
    val track2Data = args.track2Data

    fun tokenizePosCard() = viewModelScope.launch {
        _isLoading.value = true

        val response = forageTerminalSdk.tokenizeCard(
            PosTokenizeCardParams(
              posForageConfig = PosForageConfig(
                merchantId = merchantId,
                sessionToken = sessionToken
              ),
              track2Data = track2Data,
              reusable = true
            )
        )

        when (response) {
            is ForageApiResponse.Success -> {
                val adapter: JsonAdapter<PaymentMethod> = moshi.adapter(PaymentMethod::class.java)
                val paymentMethod = adapter.fromJson(response.data)

                // (optional) do something with the ref
                saveToPaymentMethodRefToMyAPI(paymentMethod.ref)
            }
            is ForageApiResponse.Failure -> {
                _error.value = response.message
            }
        }

        _isLoading.value = false
    }
}

Example PosPaymentMethod class

Note that the response object takes the same shape no matter what tokenization method you choose.

Check a card's balance

To check the balance of a card, call checkBalance() on a Forage Terminal SDK instance, passing CheckBalanceParams.

CheckBalanceParams

data class CheckBalanceParams(
    val foragePinEditText: ForagePINEditText,
    val paymentMethodRef: String
)
  • foragePinEditText (required): A reference to a ForagePINEditText instance.
  • paymentMethodRef (required): A unique string identifier for a previously created PaymentMethod in Forage's database, found in the response from a call to tokenize the card, or the Create a PaymentMethod endpoint.

Example checkBalance(checkBalanceParams) request

// BalanceCheckViewModel.kt
class BalanceCheckViewModel : ViewModel() {
    val paymentMethodRef = "020xlaldfh"

    fun checkBalance(foragePinEditText: ForagePINEditText) = viewModelScope.launch {
        val response = forageTerminalSdk.checkBalance(
            CheckBalanceParams(
                foragePinEditText = foragePinEditText,
                paymentMethodRef = paymentMethodRef
            )
        )

        when (response) {
            is ForageApiResponse.Success -> {
                // response.data will have a .snap and a .cash value
            }
            is ForageApiResponse.Failure -> {
                // do something with error text (i.e. response.message)
            }
        }
    }
}

Capture a payment

With the Forage Terminal SDK, you can opt to either defer payment capture to the server, or capture a payment immediately.

Collect a card PIN to defer payment capture to the server

Step 1: Collect a cardholder’s PIN (front-end)

Call the deferPaymentCapture(DeferPaymentCaptureParams) method on a Forage Terminal SDK instance, passing DeferPaymentCaptureParams, to collect a cardholder’s PIN for future server-side payment capture.

DeferPaymentCaptureParams

data class DeferPaymentCaptureParams(
    val foragePinEditText: ForagePINEditText,
    val paymentRef: String
)
  • foragePinEditText (required): A reference to a ForagePINEditText instance.
  • paymentRef (required): A unique string identifier for a previously created Payment in Forage's database, returned by the Create a Payment endpoint.

Example deferPaymentCapture(DeferPaymentCaptureParams) request

// DeferPaymentCaptureViewModel.kt
class DeferPaymentCaptureViewModel  : ViewModel() {
    val snapPaymentRef = "s0alzle0fal"
    val merchantId = "mid/<merchant_id>"
    val sessionToken = "<session_token>"

    fun deferPaymentCapture(foragePinEditText: ForagePINEditText, paymentRef: String) =
        viewModelScope.launch {
            val response = forageTerminalSdk.deferPaymentCapture(
                DeferPaymentCaptureParams(
                    foragePinEditText = foragePinEditText,
                    paymentRef = snapPaymentRef
                )
            )

            when (response) {
                is ForageApiResponse.Success -> {
                    // there will be no financial affects upon success
                    // you need to capture from the server to formally
                    // capture the payment
                }
                is ForageApiResponse.Failure -> {
                    // handle an error response here
                }
            }
        }
}
Step 2: Capture the payment server-side

To complete the payment capture, send a POST from your backend to /api/payments/{payment_ref}/capture_payment/ , as in the below example:

curl --request POST \
     --url https://api.sandbox.joinforage.app/api/payments/45e3f12a90/capture_payment/ \
     --header 'Authorization: Bearer sandbox_sZawPSUSm9eetx8LrfBbJlzUZS3zWD' \
     --header 'Idempotency-Key: 123e4567e8' \
     --header 'Merchant-Account: mid/123ab45c67' \
     --header 'accept: application/json'

📘

Capture a payment immediately

To capture a payment immediately, call the capturePayment(CapturePaymentParams) method on a Forage Terminal SDK instance.

CapturePaymentParams

data class CapturePaymentParams(
    val foragePinEditText: ForagePINEditText,
    val paymentRef: String
)
  • foragePinEditText (required): A reference to a ForagePINEditText instance.
  • paymentRef (required): A unique string identifier for a previously created Payment in Forage's database, returned by the Create a Payment endpoint.

Example capturePayment(CapturePaymentParams) request

// PaymentCaptureViewModel.kt
class PaymentCaptureViewModel : ViewModel() {
    val snapPaymentRef = "s0alzle0fal"
    val merchantId = "mid/<merchant_id>"
    val sessionToken = "<session_token>"

    fun capturePayment(foragePinEditText: ForagePINEditText, paymentRef: String) =
        viewModelScope.launch {
            val response = forageTerminalSdk.capturePayment(
                CapturePaymentParams(
                    foragePinEditText = foragePinEditText,
                    paymentRef = snapPaymentRef
                )
            )

            when (response) {
                is ForageApiResponse.Success -> {
                    // handle successful capture
                }
                is ForageApiResponse.Failure -> {
                    val error = response.errors[0]

                    // handle Insufficient Funds error
                    if (error.code == "ebt_error_51") {
                        val details = error.details as ForageErrorDetails.EbtError51Details
                        val (snapBalance, cashBalance) = details

                        // do something with balances ...
                    }
                }
            }
        }
}

Void a payment

Server-side

To void a payment, send a request from your backend to /api/payments/{payment_ref}/void/, as in the following example:

curl --request POST \
     --url https://api.sandbox.joinforage.app/api/payments/45e3f12a90/void/ \
     --header 'Authorization: Bearer sandbox_sZawPSUSm9eetx8LrfBbJlzUZS3zWD' \
     --header 'Idempotency-Key: 123e4567e8' \
     --header 'Merchant-Account: mid/123ab45c67' \
     --header 'accept: application/json'

📘

Refund a payment

Whether you process a refund immediately or defer refund completion to the server, if a refund attempt fails then prompt the customer to repeat the request.

Process a refund immediately

To refund an EBT POS payment immediately, pass PosRefundPaymentParams to the refundPayment() method.

data class PosRefundPaymentParams(
  val foragePinEditText: ForagePINEditText,
  val paymentRef: String,
  val amount: Float,
  val reason: String,
  val metadata: Map<String, String>? = null
)

suspend fun refundPayment(
    params: RefundPaymentParams
): ForageApiResponse<String>

PosRefundPaymentParams

  • foragePinEditText: A reference to a ForagePINEditText component.
  • paymentRef: A unique string identifier for a previously created Payment in Forage's database, returned by the Create a Payment endpoint.
  • amount: A positive decimal number that represents how much of the original payment to refund in USD. Precision to the penny is supported. The minimum amount that can be refunded is 0.01.
  • reason: A string that describes why the payment is to be refunded.
  • metadata: A set of optional, merchant-defined key-value pairs. For example, some merchants attach their credit card processor’s ID for the customer making the refund.

Example refundPayment request

// PosRefundViewModel.kt

class PosRefundViewModel : ViewModel() {
  var paymentRef: String  = ""
  var amount: Float = 0.0
  var reason: String = ""
  var metadata: HashMap? = null
  
  
  fun refundPayment(foragePinEditText: ForagePINEditText) = viewModelScope.launch {
    val refundParams = PosRefundPaymentParams(
      foragePinEditText,
      paymentRef, 
      amount,
      reason,
      metadata,
    )
    val response = forageTerminalSdk.refundPayment(refundParams)
    
    when (response) {
      is ForageApiResponse.Success -> {
        // do something with response.data
      }
      is ForageApiResponse.Failure -> {
        // do something with response.errors
      }
    }
  }
}

Defer refund completion to the server

Step 1: Collect a customer’s EBT Card PIN (front-end)

After initializing the Terminal SDK and creating a Forage Element, pass PosDeferPaymentRefundParams to the deferPaymentRefund() method.

data class PosDeferPaymentRefundParams(
    val foragePinEditText: ForagePINEditText,
    val paymentRef: String
)

suspend fun deferPaymentRefund(
    params: PosDeferPaymentRefundParams
): ForageApiResponse<String>

PosDeferPaymentRefundParams

  • foragePinEditText: A reference to a ForagePINEditText component.
  • paymentRef: A unique string identifier for the previously created Payment in Forage's database, returned by the Create a Payment endpoint when the payment was first created.

Example deferPaymentRefund request:

// PosDeferPaymentRefundViewModel.kt

class PosDeferPaymentRefundViewModel : ViewModel() {
  var paymentRef: String  = ""

  fun deferPaymentRefund(foragePinEditText: ForagePINEditText) = viewModelScope.launch {
    val deferPaymentRefundParams = PosDeferPaymentRefundParams(
      foragePinEditText,
      paymentRef
    )
    val response = forageTerminalSdk.deferPaymentRefund(deferPaymentRefundParams)

    when (response) {
      is ForageApiResponse.Success -> {
        // do something with response.data
      }
      is ForageApiResponse.Failure -> {
        // do something with response.errors
      }
    }
  }

📘

Terminal SDK reference docs

Step 2: Complete the Refund (server-side)

Send a POST to /payments/{payment_ref}/refunds/ to complete the refund. Pass the amount, reason, provider_terminal_id, and metadata in the body of the request.

curl --request POST \
     --url https://api.sandbox.joinforage.app/api/payments/{payment_ref}/refunds/ \
     --header 'Authorization: Bearer <authentication_token>' \
     --header 'Idempotency-Key: <idempotency-key>' \
     --header 'Merchant-Account: mid/<merchant-id>' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "amount": "<amount>",
  "reason": "<reason>",
  "pos_terminal": {
    "provider_terminal_id": "<provider_terminal_id>"
  },
  "metadata": {}
}
'

📘

Payments API reference docs

Example Refund and Receipt class

Note that the response object takes the same shape no matter what refund method you choose.

Void a refund

Server-side

To void a refund, send a request from your backend to /api/payments/{payment_ref}/refunds/{refund_ref}/void/, as in the following example:

curl --request POST \
     --url https://api.sandbox.joinforage.app/api/payments/45e3f12a90/refunds/b873fe62dc/void/ \
     --header 'Authorization: Bearer sandbox_sZawPSUSm9eetx8LrfBbJlzUZS3zWD' \
     --header 'Idempotency-Key: 123e4567e8' \
     --header 'Merchant-Account: mid/123ab45c67' \
     --header 'accept: application/json'

📘