Skip to main content
Version: Next

Contactless Payment

This guide describes the contactless payment flow where a Customer device connects to a specific Business terminal identified by a tag ID (e.g., via NFC/QR code scan) for direct payment.

Prerequisites

  • SDK initialized and configured. See: Android VipasoSDK
  • Business user and Customer user are authenticated on their respective devices
  • Business terminal tag ID obtained (e.g., from NFC tap or QR scan)

Flow overview

  1. Customer obtains the Business terminal tag ID (NFC/QR)
  2. Customer starts a direct payment with that specific terminal
  3. Business sends a payment request to that specific Customer
  4. The payment request is shown to the Customer, who can accept or cancel
  5. If accepted, the payment is executed and both Customer and Business receive a successful payment result

1) Business — Send contactless payment request

The Business device sends a direct payment request to a specific Customer device identified by tag ID.

Note: The payment remains active until it reaches a final state (successful, cancelled, or failed), after which it is automatically terminated.

import io.vipaso.vipaso.sdkApi.payment.business.response.VipasoPaymentResult
import java.util.UUID

val tagId = UUID.fromString("customer-terminal-tag-id")
val result: VipasoPaymentResult = Vipaso.payment.business.sendContactlessPaymentRequest(
tagId = tagId,
amount = "25.50",
currency = "USD",
paymentReference = "order-123" // optional
)

when (result) {
is VipasoPaymentResult.PaymentSuccessful -> {
val id = result.paymentId
val amount = result.amount
val tip = result.tip
val currency = result.currency
// Handle success - payment automatically terminates
}
is VipasoPaymentResult.PaymentCancelled -> {
val id = result.paymentId
// Handle cancellation - payment automatically terminates
}
is VipasoPaymentResult.PaymentFailed -> {
val id = result.paymentId
val error = result.error
// Handle failure - payment automatically terminates
}
}

2) Customer — Listen for contactless payments from a specific terminal

The Customer device starts a direct BLE payment with a specific Business terminal by providing its tag ID.

Note: This starts a payment that remains active until it reaches a final state. To receive another payment, call the receive method again.

import io.vipaso.vipaso.sdkApi.payment.customer.response.VipasoCustomerPaymentEvent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import java.util.UUID

// Start contactless payment with a specific terminal
val tagId = UUID.fromString("terminal-tag-id")
val contactlessPaymentsFlow: Flow<VipasoCustomerPaymentEvent> = Vipaso.payment.customer.receiveContactlessPaymentRequest(tagId)

scope.launch {
contactlessPaymentsFlow.collect { event ->
when (event) {
is VipasoCustomerPaymentEvent.PaymentRequestReceived -> {
val paymentId = event.paymentId
val amount = event.amount
val currency = event.currency
val businessUserId = event.businessUserID
val operatorId = event.operatorId
val businessUserName = event.businessUserName
// You can accept or decline the payment request
}
is VipasoCustomerPaymentEvent.PaymentSuccessful -> {
// Payment was successful, show confirmation
// Payment automatically terminates after this event
}
is VipasoCustomerPaymentEvent.PaymentCancelled -> {
// Payment was cancelled
// Payment automatically terminates after this event
}
is VipasoCustomerPaymentEvent.AnotherUserPaid -> {
// Another user paid instead
// Payment automatically terminates after this event
}
is VipasoCustomerPaymentEvent.PaymentFailed -> {
// Payment failed, show error message
// Payment automatically terminates after this event
}
}
}
}

Accept Payment

When an incoming payment request arrives via contactless payment, users can decide if they accept it. If the user accepts, send the acceptance to the Business device.

Instrument Types:

  • Non-Delegated Instrument (isDelegatedInstrument = false): An instrument created and managed on Vipaso using the Instruments API.
  • Delegated Instrument (isDelegatedInstrument = true): An instrument created and managed on your own backend or app.
// Request: Accept a payment from a contactless payment request
val acceptance = VipasoPaymentAcceptanceRequest(
paymentId = UUID.fromString("payment-id-from-request"),
amount = "50.00",
tip = "5.00", // optional
currency = "USD",
instrumentId = "selected-instrument-id",
isDelegatedInstrument = true // true: instrument managed by your own backend/app; false: instrument created on Vipaso via Instruments API
)

Vipaso.payment.customer.acceptPayment(acceptance)

// The payment result will be delivered through the receiveContactlessPaymentRequest() flow
// as PaymentSuccessful, PaymentFailed, or other events

Note: The acceptance method call is the same for both delegated and non-delegated instruments. Only the isDelegatedInstrument parameter changes. For non-delegated instruments, see the Instruments API.

Cancel Payment (Business)

The Business can cancel the current payment, which terminates the payment on both Business and Customer sides:

// Cancel current payment and automatically terminate payment on both sides
Vipaso.payment.business.cancelPayment()

Important: When business cancels a payment, all connected customers are notified that the payment is cancelled and cannot pay it anymore and their payments are automatically terminated.

Cancel Payment (Customer)

If the Customer does not accept a payment, they can cancel the operation:

// Request: Reject or cancel a payment operation
Vipaso.payment.customer.cancelPayment("payment_id")

// This gracefully cancels the payment and automatically terminates the payment
// The payment state will be updated in the contactless payment flow

Important: When you cancel a payment, the customer will not receive this payment request from the business side anymore.


3) Termination

To manually terminate the payment at any time (e.g., user navigates away, app lifecycle events):

// Manually terminate payment (idempotent - safe to call anytime)
Vipaso.payment.customer.terminatePayment()

Best Practice: Always call terminatePayment() in cleanup blocks (e.g., in finally blocks or onDestroy(), onCleared(), etc.). The method is idempotent and won't cause issues if the payment is already terminated or being terminated