HomeGuidesReference
Log In
Guides

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

  1. Create a placeholder Payment (server-side)
  2. Collect a customer’s EBT Card PIN to defer payment capture to the server (front-end)
  3. Update the Payment (server-side)
  4. Capture the Payment (server-side)

Defer refund completion to the server

  1. Collect a customer's EBT Card PIN (front-end)
  2. Complete the PaymentRefund (server-side)

Prerequisites

Before you begin, make sure that you've completed the following:

Defer payment capture to the server

Step 1: Create a placeholder 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)

🚧

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)

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

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