Defer EBT payment capture and refund completion to the server
Learn how to use the Forage Payments API and SDKs to defer EBT payment capture and refund completion to the server.
In some cases, a merchant might not have all of the information that they need to instantly process a payment or refund. Forage provides endpoints and SDK methods to create a Payment
or PaymentRefund
while holding off on populating the transaction details or capturing the charge. In this guide you’ll learn how to:
Defer payment capture to the server
- Create a placeholder
Payment
(server-side) - Collect a customer’s EBT Card PIN to defer payment capture to the server (front-end)
- Update the
Payment
(server-side) - Capture the
Payment
(server-side)
Defer refund completion to the server
- Collect a customer's EBT Card PIN (front-end)
- Complete the
PaymentRefund
(server-side)
Prerequisites
Before you begin, make sure that you've completed the following:
- Sign up for a Forage account (contact us if you don’t yet have one)
- Register an app
- Generate an authentication token and a session token
- Set up your development environment. Install and initialize the SDK of your choice. Consult the relevant quickstart guide for instructions:
Defer payment capture to the server
Step 1: Create a placeholder Payment
(server-side)
Payment
(server-side)Send a POST
to /payments/
to create a placeholder Payment
. When you create a placeholder Payment
, the only required body parameters are funding_type
, payment_method
, description
, and metadata
:
curl --request POST \
--url https://api.sandbox.joinforage.app/api/payments/ \
--header 'Authorization: Bearer <authentication-token>' \
--header 'Idempotency-Key: 123e4567e8' \
--header 'Merchant-Account: 9000055' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"funding_type": "ebt_snap",
"payment_method": "<payment-method-ref>",
"description": "This is an EBT payment",
"metadata": {}
}
'
Store the ref
returned in the response. You’ll need it to update the Payment
in Step 3.
Step 2: Collect a customer’s EBT Card PIN (front-end)
SDK reference docs
After initializing the SDK of your choice and creating a Forage Element, call the method that collects a customer’s EBT Card PIN for future payment capture.
On success, the PIN is encrypted and associated with the Payment in Forage’s vault. There is no body in the SDK response.
If there’s a problem submitting the request, then the SDK returns an an error object that includes a code
and message
pair.
The following snippets demonstrate the defer payment capture function call for each SDK, along with placeholder error handling code:
try {
await forage.deferPaymentCapture(
deferPaymentCaptureElement,
paymentRef
)
} catch (error) {
const { httpStatusCode, message, code } = error ?? {}
switch (code) {
case 'user_error':
// handle invalid user input
break
case 'resource_not_found':
// handle payment not found
break
// other errors...
default:
// handle unexpected errors
}
}
ForageSDK.shared.deferPaymentCapture(
foragePinTextField: foragePinTextField,
paymentReference: paymentReference
) { result in
switch result {
case .success:
// handle successful PIN collection
case let .failure(error):
if let forageError = error as? ForageError {
switch forageError.code {
case "user_error":
// handle invalid user input
case "resource_not_found":
// handle payment not found
default:
// handle unknown error
}
}
}
}
val response = ForageSDK().deferPaymentCapture(
DeferPaymentCaptureParams(
foragePinEditText = pinForageEditText,
paymentRef = paymentRef
)
)
when (response) {
is ForageApiResponse.Success -> {
// handle successful response
}
is ForageApiResponse.Failure -> {
// handle error response
handleErrorResponse(response.errors)
}
}
// somewhere else
fun handleErrorResponse(errors: List<ForageError>) {
val error = errors.firstOrNull()
when (error?.code) {
"user_error" -> {
// handle invalid user input
}
"resource_not_found" -> {
// handle payment not found
}
else -> {
// handle unknown error
}
}
}
// 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
}
}
}
}
Errors reference documentation
All possible errors that
deferPaymentCapture
can return are marked in the Payments API SDK errors documentation.
How to programmatically collect pins during sandbox testing
To speed up testing and development, you can send a POST
to /payments/{payment_ref}/collect_pin_backend/
to programmatically associate a test PIN with an existing Payment
object.
Example request:
curl --request POST \
--url https://api.sandbox.joinforage.app/api/payments/{payment_ref}/collect_pin_backend/ \
--header 'Authorization: Bearer <authentication-token>' \
--header 'Idempotency-Key: 123e4567e8' \
--header 'Merchant-Account: 9000055' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"pin": "1234",
}
'
Example response:
{}
Only use the
/collect_pin_backend/
endpoint during sandbox testing. It can't be used in production. Check out the reference documentation for details.
Step 3: Update the Payment
(server-side)
Payment
(server-side)The PATCH endpoint is in active development.
Documentation is subject to change. We appreciate your patience, feedback, and partnership as we continue building together.
After you have all of the required information to process the payment, send a PATCH
to /payments/{payment_ref}/
to update the Payment
:
curl --request PATCH \
--url https://api.sandbox.joinforage.app/api/payments/{payment_ref}/ \
--header 'Authorization: Bearer <authentication-token>' \
--header 'Idempotency-Key: 123e4567e8' \
--header 'Merchant-Account: 9000055' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"delivery_address": {
"city": "San Francisco",
"country": "US",
"line1": "1856 Market St.",
"line2": "Unit 3",
"zipcode": "94106",
"state": "CA"
},
"is_delivery": false,
"amount": 20.00
}
'
Step 4: Capture the Payment
(server-side)
Payment
(server-side)A POST
to /payments/{payment_ref}/capture_payment/
captures the Payment
.
On success, Forage responds with the Payment
object and immediately begins processing the charge.
On failure, the API returns an an error object that includes a code
and message
pair.
The following snippets demonstrate the request. The sample servers include placeholder error handling instructions.
curl --request POST \
--url https://api.sandbox.joinforage.app/api/payments/{payment_ref}/capture_payment/ \
--header 'Authorization: Bearer <token>' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'Merchant-Account: <merchant-account>' \
--header 'accept: application/json'
// 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 and JSON response
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.post('/api/capture_payment', async (req, res) => {
const { paymentRef } = req.body
const apiUrl = `https://api.sandbox.joinforage.app/payments/${paymentRef}/capture_payment/`
// 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, // must be in format of "mid/<merchant_ref>"
'Accept': 'application/json'
}
try {
const capturedPayment = await axios.post(apiUrl, null, { headers }).data
} catch (error) {
if (error.response) {
const uncapturedPayment = error.response.data
const [firstError] = uncapturedPayment.errors ?? []
handleErrorResponse(firstError)
}
}
})
function handleErrorResponse(error) {
switch (error?.code) {
case 'ebt_error_52':
// handle inactive card
break
case 'resource_not_found':
// handle payment not found
break
case 'ebt_error_51'
// handle insufficient funds case this special error
// includes the updated balances of the user's EBT Card
const { message } = error
const {
cash_balance, // "1000.00"
snap_balance, // "1000.00"
} = error.details
break
// handle any other errors you wish
default:
// handle unexpected error type
}
}
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`)
})
# app.rb
require 'sinatra'
require 'rest-client'
require 'json'
require 'dotenv/load'
# Middleware to parse JSON bodies
use Rack::Parser, content_type: 'application/json'
post '/api/capture_payment' do
begin
payment_ref = JSON.parse(request.body.read)['paymentRef']
api_url = "https://api.sandbox.joinforage.app/payments/#{payment_ref}/capture_payment/"
# 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']
headers = {
'Authorization' => "Bearer #{authentication_token}",
'Merchant-Account' => merchant_id,
'Accept' => 'application/json'
}
captured_payment = RestClient.post(api_url, nil, headers)
# do something with success
rescue RestClient::ExceptionWithResponse => e
uncaptured_payment = JSON.parse(e.response.body)
if uncaptured_payment['errors'] && uncaptured_payment['errors'].any?
first_error = parsed_response['errors'].first
handle_error_response(first_error)
end
end
end
def handle_error_response(error)
case error['code']
when 'ebt_error_52'
# handle inactive card
when 'resource_not_found'
# handle payment not found
when 'ebt_error_51'
# Handle insufficient funds case this special error
# includes the updated balances of the user's EBT Card
message = error["message"]
cash_balance = error["details"]["cash_balance"] # "1000.00"
snap_balance = error["details"]["snap_balance"] # "1000.00"
# Add your handling code here
end
else
# handle unexpected error type
end
end
status status
JSON.generate(error_details)
end
On success, Forage responds with the Payment
object and immediately begins processing the charge.
How to handle errors
If the transaction fails to process and Forage returns an error, then retry the same request with the original payment_ref
. There is no need to create a new placeholder Payment
.
If the error persists, then inspect the message
field of the response for error handling suggestions.
Errors reference documentation
Refer to the Payments API SDK errors documentation for a complete list of possible code and message pairs.
Defer refund completion to the server (POS Terminal only)
For more details on integrating Forage Terminal, check out the POS Terminal Quickstart.
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()
function.
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 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
}
}
}
Step 2: Complete the Refund (server-side)
Send a POST
to /payments/{payment_ref}/refunds/
to complete the refund. Pass the amount
, reason
, metadata
, and provider_terminal_id
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-account>' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"amount": "<amount>",
"reason": "<reason>",
"metadata": "<metadata>",
"pos_terminal": {
"provider_terminal_id": "<provider_terminal_id>"
}
}
'
Payments API reference docs
Updated 5 months ago