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

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
    // MERCHANT_ID format is mid/{merchant_id}, e.g. mid/0a1b2c3d4e
    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

Install the Android POS SDK from build artifacts.

  1. Download the forage-android-release.aar and the pom-default.xml files from the latest release for the Forage Android POS Terminal SDK

  2. Add those files to a local directory called libs/forage-android-pos-terminal-sdk inside your app’s directory.

    1. You can put this elsewhere if you want, but you’ll need to adjust the paths in the gradle snippets later.
  3. In your build.gradle.kts file for your app (not the entire project), add this line to your dependencies section

    // adjust the path as needed depending on where you saved this in Step 2
    implementation(files("libs/forage-android-pos-terminal-sdk/forage-android-release.aar"))
    
  4. To resolve the dependencies of the Forage POS SDK, you can either manually add them to your dependencies by reading the <dependencies> section of the pom-default.xml file, or you can add this script to the end of your build.gradle.kts, which will handle it for you —

import org.w3c.dom.Element
import javax.xml.parsers.DocumentBuilderFactory

// ...the rest of your existing build.gradle...

fun parsePomDependencies(pomFile: File): List<String> {
    val dependencies = mutableListOf<String>()
    val dbFactory = DocumentBuilderFactory.newInstance()
    val dBuilder = dbFactory.newDocumentBuilder()
    val doc = dBuilder.parse(pomFile)

    val depNodes = doc.getElementsByTagName("dependency")
    for (i in 0 until depNodes.length) {
        val node = depNodes.item(i) as Element
        val groupId = node.getElementsByTagName("groupId").item(0).textContent
        val artifactId = node.getElementsByTagName("artifactId").item(0).textContent
        val version = node.getElementsByTagName("version").item(0).textContent
        dependencies.add("$groupId:$artifactId:$version")
    }
    return dependencies
}

// Register a task to import the dependencies of the forage sdk
tasks.register("importForageDependencies") {
    doLast {
      // adjust this path if you saved the files in a different location
        val pomFile = file("libs/forage-android-pos-terminal-sdk/pom-default.xml") // Adjust the path if needed
        if (pomFile.exists()) {
            val dependencies = parsePomDependencies(pomFile)
            dependencies.forEach { dependency ->
                println("Adding Forage dependency: $dependency")
                dependencies {
                    add("implementation", dependency)
                }
            }
        } else {
            println("POM file not found: $pomFile")
        }
    }
}

// Run the task before compiling the project
tasks.whenTaskAdded {
    if (name.startsWith("compile")) {
        dependsOn("importForageDependencies")
    }
}
  1. Sync your updated build.gradle.kts and you should be all set!
    1. Note: If you change the assets in the libs/forage-android-pos-terminal-sdk directory (like when updating to a new release, for instance), it may not immediately get picked up in Android Studio. Commenting out the implementation(files("libs/forage-android-pos-terminal-sdk/forage-android-release.aar")) line, syncing (thus temporarily removing the sdk), then un-commenting that line and syncing again should fully pick up any changes.

Step 2: Add and style a Forage Element

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 a card number, ForagePANEditText, and two Element options for collecting the PIN, ForagePINEditText and ForagePinPad. Use the ForagePinPad if your terminal supports a guest facing display or if you expect to display the PIN pad on an external monitor.

You add Elements to your layout file. The following example adds a ForagePINEditText:

<!-- forage_pin_component.xml -->

<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.pos.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"
        app:elementWidth="match_parent"
        app:elementHeight="wrap_content"
    />

</androidx.constraintlayout.widget.ConstraintLayout>

The below example adds a 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.pos.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>

Finally, the following adds a ForagePinPad:

<!-- forage_pin_pad_component.xml -->
<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.pos.ui.element.ForagePinPad
        android:id="@+id/my_pin_pad"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:forage_buttonLayoutMargin="@dimen/keypad_btn_margin"
        app:forage_buttonLayoutHeight="@dimen/keypad_btn_height"
        app:forage_deleteButtonIcon="@android:drawable/ic_delete"
     />
  
</androidx.constraintlayout.widget.ConstraintLayout>

Style a Forage 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

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.

Pass the returned session token, along with the merchant account number, as the ForageConfig in a call to ForageTerminalSDK.init().

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

val forage = ForageTerminalSDK.init(
	context,
	posTerminalId,
	ForageConfig(merchantId, sessionToken)
)

🚧

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.

(For ForagePANEditText tokenize a card operations-only):** Pass the returned session token, along with the merchant account number, in a call to setForageConfig()

setForageConfig() sets foundational settings for a ForagePANEditText, passed as the ForageConfig parameter.

You must call setForageConfig() on an ForagePANEditText Element before invoking any other methods, as in the below example:

val foragePanEditText = root?.findViewById<ForagePANEditText>(R.id.foragePanEditText)
foragePanEditText.setForageConfig(
    ForageConfig(
        sessionToken = 'sandbox_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...',
        merchantId = 'mid/123ab45c67'
    )
)

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

Use a ForagePANEditText Element to 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 TokenizeManualEntryParams.

TokenizeManualEntryParams

data class TokenizeManualEntryParams(
    val foragePanEditText: ForagePANEditText,
    val reusable: Boolean = true
)
Example tokenizeCard() request
// TokenizeViewModel.kt
@HiltViewModel
class TokenizeViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
) : 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() = viewModelScope.launch {
        _isLoading.value = true

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

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

                // (optional) do something with the ref
                savePaymentMethodRefToMyAPI(paymentMethod.ref)
            }
            is ForageApiResponse.Failure -> {
                // Unpack response.errors
            }
        }

        _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 TokenizeMagSwipeParams.

TokenizeMagSwipeParams
data class TokenizeMagSwipeParams(
    val forageConfig: ForageConfig,
    val track2Data: String,
    val reusable: Boolean = true
)
  • ForageConfig (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(TokenizeMagSwipeParams) request

// TokenizePosViewModel.kt
@HiltViewModel
class TokenizePosViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
) : 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 tokenizeCard() = viewModelScope.launch {
        _isLoading.value = true

        val response = forageTerminalSdk.tokenizeCard(
            TokenizeMagSwipeParams(
              forageConfig = ForageConfig(
                merchantId = merchantId,
                sessionToken = sessionToken
              ),
              track2Data = track2Data,
              reusable = true
            )
        )

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

                // (optional) do something with the ref
                savePaymentMethodRefToMyAPI(paymentMethod.ref)
            }
            is ForageApiResponse.Failure -> {
                // Unpack response.errors
            }
        }

        _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

Use either a ForagePINEditText or a ForagePinPad Element to check the balance of an EBT Card.

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

CheckBalanceParams

data class CheckBalanceParams(
    val forageVaultElement: ForageVaultElement,
    val paymentMethodRef: String
)
  • forageVaultElement (required): A reference to either a ForagePINEditText or a ForagePinPad 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(forageVaultElement: ForagePINEditText) = viewModelScope.launch {
        val response = forageTerminalSdk.checkBalance(
            CheckBalanceParams(
                forageVaultElement = forageVaultElement,
                paymentMethodRef = paymentMethodRef
            )
        )

        when (response) {
            is ForageApiResponse.Success -> {
                val balance = response.toBalance() as EbtBalance
                if (balance is EbtBalance) {
                    // Unpack balance.snap, balance.cash
                }
            }
            is ForageApiResponse.Failure -> {
                // Unpack response.errors
            }
        }
    }
}

Capture a payment

Use either a ForagePINEditText or a ForagePinPad Element to 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 val forageVaultElement: ForageVaultElement,
    val paymentRef: String
)
  • forageVaultElement (required): A reference to either a ForagePINEditText or a ForagePinPad 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(forageVaultElement: ForagePINEditText, paymentRef: String) =
        viewModelScope.launch {
            val response = forageTerminalSdk.deferPaymentCapture(
                DeferPaymentCaptureParams(
                    forageVaultElement = forageVaultElement,
                    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 -> {
                    // Unpack response.errors
                }
            }
        }
}
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 forageVaultElement: ForageVaultElement,
    val paymentRef: String
)
  • forageVaultElement (required): A reference to either a ForagePINEditText or a ForagePinPad 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(forageVaultElement: ForagePINEditText, paymentRef: String) =
        viewModelScope.launch {
            val response = forageTerminalSdk.capturePayment(
                CapturePaymentParams(
                    foragePinEditText = foragePinEditText,
                    paymentRef = snapPaymentRef
                )
            )

            when (response) {
                is ForageApiResponse.Success -> {
                    // handle successful capture
                    val payment = response.toPayment()
                    // Unpack payment.ref, payment.amount, ...
                }
                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

Use either a ForagePINEditText or a ForagePinPad Element to 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 RefundPaymentParams to the refundPayment() method.

data class RefundPaymentParams(
  val forageVaultElement: ForageVaultElement,
  val paymentRef: String,
  val amount: Float,
  val reason: String,
  val metadata: Map<String, String>? = null
)

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

RefundPaymentParams

  • forageVaultElement (required): A reference to either a ForagePINEditText or a ForagePinPad instance.
  • 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(forageVaultElement: ForagePINEditText) = viewModelScope.launch {
    val refundParams = RefundPaymentParams(
      forageVaultElement,
      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 DeferPaymentRefundParams to the deferPaymentRefund() method.

data class DeferPaymentRefundParams(
    val forageVaultElement: ForageVaultElement,
    val paymentRef: String
)

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

DeferPaymentRefundParams

  • forageVaultElement (required): A reference to either a ForagePINEditText or a ForagePinPad instance.
  • 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(forageVaultElement: ForagePINEditText) = viewModelScope.launch {
    val deferPaymentRefundParams = DeferPaymentRefundParams(
      forageVaultElement,
      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'

📘