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:
- Set up a Forage Android app
- Add and style a Forage Element
- Configure a Forage Terminal instance
- 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
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.
-
Download the
forage-android-release.aar
and thepom-default.xml
files from the latest release for the Forage Android POS Terminal SDK -
Add those files to a local directory called
libs/forage-android-pos-terminal-sdk
inside your app’s directory.- You can put this elsewhere if you want, but you’ll need to adjust the paths in the gradle snippets later.
-
In your
build.gradle.kts
file for your app (not the entire project), add this line to yourdependencies
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"))
-
To resolve the dependencies of the Forage POS SDK, you can either manually add them to your dependencies by reading the
<dependencies>
section of thepom-default.xml
file, or you can add this script to the end of yourbuild.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")
}
}
- Sync your updated
build.gradle.kts
and you should be all set!- 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 theimplementation(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.
- Note: If you change the assets in the
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()
.
ForageConfig
in a call to ForageTerminalSDK.init()
.ForageTerminalSDK.init()
creates a Forage Terminal instance. Pass the Context
, posTerminalId
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()
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 = '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:
- Tokenize a card
- Using a
ForagePANEditText
Element - Via a magnetic swipe
- Using a
- Check a card's balance
- Capture a payment
- Collect a card PIN to defer payment capture to the server
- Capture a payment immediately
- Void a payment
- Refund a payment
- Process a refund immediately
- Defer refund completion to the server
- Void a refund
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
ForagePANEditText
ElementTo tokenize a card via a a ForagePANEditText
Element, call tokenizeCard()
on a ForageTerminalSDK
instance, passing TokenizeManualEntryParams
.
TokenizeManualEntryParams
TokenizeManualEntryParams
data class TokenizeManualEntryParams(
val foragePanEditText: ForagePANEditText,
val reusable: Boolean = true
)
Example tokenizeCard()
request
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
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, as in123ab45c67
. 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 totrue
by default.
Example tokenizeCard(TokenizeMagSwipeParams)
request
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 aForagePinPad
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
CheckBalanceParams
data class CheckBalanceParams(
val forageVaultElement: ForageVaultElement,
val paymentMethodRef: String
)
forageVaultElement
(required): A reference to either aForagePINEditText
or aForagePinPad
instance.paymentMethodRef
(required): A unique string identifier for a previously createdPaymentMethod
in Forage's database, found in the response from a call to tokenize the card, or the Create aPaymentMethod
endpoint.
Example checkBalance(checkBalanceParams)
request
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 aForagePinPad
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 aForagePINEditText
or aForagePinPad
instance.paymentRef
(required): A unique string identifier for a previously createdPayment
in Forage's database, returned by the Create aPayment
endpoint.
Example deferPaymentCapture(DeferPaymentCaptureParams)
request
// DeferPaymentCaptureViewModel.kt
class DeferPaymentCaptureViewModel : ViewModel() {
val snapPaymentRef = "s0alzle0fal"
val merchantId = "<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: 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 aForagePINEditText
or aForagePinPad
instance.paymentRef
(required): A unique string identifier for a previously createdPayment
in Forage's database, returned by the Create aPayment
endpoint.
Example capturePayment(CapturePaymentParams)
request
// PaymentCaptureViewModel.kt
class PaymentCaptureViewModel : ViewModel() {
val snapPaymentRef = "s0alzle0fal"
val merchantId = "<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: 123ab45c67' \
--header 'accept: application/json'
Refund a payment
Use either a
ForagePINEditText
or aForagePinPad
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
RefundPaymentParams
forageVaultElement
(required): A reference to either aForagePINEditText
or aForagePinPad
instance.paymentRef
: A unique string identifier for a previously createdPayment
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 is0.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
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 aForagePINEditText
or aForagePinPad
instance.paymentRef
: A unique string identifier for the previously createdPayment
in Forage's database, returned by the Create aPayment
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: <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: 123ab45c67' \
--header 'accept: application/json'
Updated 3 months ago