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

  • A logged-in business user
  • A logged-in customer with at least one valid payment instrument
  • A Business terminal tag UUID to target the specific payment terminal

Flow overview

  1. Customer starts advertising via BLE with a specific tag ID and listens for payment events
  2. Business side creates a payment instance targeting the specific tag UUID and scans for BLE connections
  3. Information exchange occurs between business and customer devices once connected
  4. Payment request is sent by the business user and can be accepted or cancelled by the customer
  5. Payment acceptance is sent from the customer to the business user if approved
  6. Payment execution happens on both sides with the backend

Offline scenarios:

  • If one side is offline, the online side handles the payment and shares results
  • If both are offline, payment requires pre-authorization (experimental feature)

Let's check each step in more detail.

Customer: Receiving Contactless Payments

The customer needs to activate the contactless BLE payment stack with a specific tag ID to receive targeted payment requests. You need to connect to the business side via BLE and register as a payment receiver to be notified about any payment requests coming from the business side.

Important: The callback closure handles multiple events. When a payment is concluded the connection is terminated.

The usual flow of these events for a payment looks like:

  1. receivedPayment
  2. finishedPayment when the payment is concluded with success
    alternate payment outcomes:
    1. cancelled in case the payment is cancelled by either side this indicates the end of a payment
    2. error in case a payment or an established payment connection fails
    3. otherPaid in case the received payment was paid by another payee
  3. disconnected when the connection is closed

Customer — Listen for contactless payments from a specific terminal

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

Note: This starts a short-lived payment session that automatically terminates after final payment events. Each session handles only a single payment - to receive another payment, call the receive method again.

let tagID = UUID().uuidString // Your specific service tag identifier

vipaso.payment.customer.receiveContactlessPaymentRequest(tagID: tagID) { paymentEvent in
switch paymentEvent {
case .cancelled:
// Handle the cancellation
self?.handlePaymentCancellation()
case .error(let error):
// Handle the error case
self?.handlePaymentError(error)
case .finishedPayment(let paymentResult):
// Handle the case the payment is finished
self?.handleFinishedPayment()
case .receivedPayment(let paymentRequest):
// display payment for the user
self?.handlePayment(paymentRequest: paymentRequest)
case .otherPaid:
// Handle the case if another wallet paid the payment
self?.handleOtherPayment()
case .disconnected:
// Handle the case if another wallet paid the payment
self?.handleDisconnected()
}
}

Example screenshots from our development application:

Business: Send contactless payment request

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

Note: This creates a temporary payment session that ends automatically after the payment is complete. Each session handles one payment only.

let tagID = UUID().uuidString // The service tag UUID to target

vipaso.payment.business.sendContactlessPaymentRequest(
amount: "25.50",
currency: "USD",
paymentReference: "order-123", // Optional reference
tagID: tagID
) { [weak self] result in
switch result {
case .success(let response):
self?.handlePaymentResponse(response)
case .failure(let error):
self?.handlePaymentError(error)
}
}

Accepting the 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 (isDelegatedInstrumentID = false): An instrument created and managed on Vipaso using the Instruments API.
  • Delegated Instrument (isDelegatedInstrumentID = true): An instrument created and managed on your own backend or app.

With instruments managed by Vipaso (Non-Delegated)

let request = VipasoPaymentAcceptanceRequest(
paymentID: paymentID,
amount: amount,
tip: tip,
currency: currency,
instrumentID: instrumentID,
createdAt: Date().iso8601String,
isDelegatedInstrumentID: false // Set to true for delegated instruments
)

vipaso.payment.customer.acceptPayment(request: request) { [weak self] result in
switch result {
case .success(let response):
self?.handleAcceptanceSuccess(response)
case .failure(let error):
self?.handleAcceptanceFailure(error)
}
}

With client Delegated instruments

For instruments managed by your own backend or app, set isDelegatedInstrumentID = true.

let request = VipasoPaymentAcceptanceRequest(
paymentID: paymentID,
amount: amount,
tip: tip,
currency: currency,
instrumentID: instrumentID,
createdAt: Date().iso8601String,
isDelegatedInstrumentID: true
)

vipaso.payment.customer.acceptPayment(request: request) { result in
switch result {
case .success(let request):
promise(.success(request))
case .failure(let error):
promise(.failure(.generic(error: error)))
}
}

NOTE: The acceptance method call is the same. Only the input model changes.

Example screenshots from our development application:

Cancelling the payment

Customer side cancellation:

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

IMPORTANT: When this call succeeds the customer will not receive this payment from the business side anymore.

vipaso.payment.customer.cancelPayment(paymentID: paymentID) { [weak self] result in
switch result {
case .success(let response):
self?.handleCancellationSuccess(response)
case .failure(let error):
self?.handleCancellationFailure(error)
}
}

Business side cancellation:

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

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

vipaso.payment.business.cancelPayment()

Termination

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

vipaso.payment.customer.terminatePayment()
//or
vipaso.payment.business.terminatePayment()

**Best Practice**: Always call `terminatePayment()` when a payment flow is concluded. The method is idempotent and won't cause issues if the session is already terminated or being terminated