Skip to main content

Terminal Payments

The Terminal Payments API lets you take in-person card payments on Kody's physical terminals. Use it to list your terminals, send a payment to a terminal, cancel a payment in progress, retrieve payment details, issue refunds (from the backend or a terminal), void a payment, and close batches.

Before you begin

Every endpoint on this page is a gRPC call secured with TLS. Authenticate each call by attaching your API key to the request metadata using the X-API-Key header.

Connect to the host closest to your store's region. See the Payments API Overview for the full list of regional hostnames (Asia-Pacific and Europe), authentication, shared conventions, and how to install the client SDK.

RegionDevelopment and TestLive
Asia-Pacificgrpc-staging-ap.kodypay.comgrpc-ap.kodypay.com
Europegrpc-staging-eu.kodypay.comgrpc-eu.kodypay.com

Use the regional staging host that matches your store in the examples below. Replace the following placeholders with your own values:

PlaceholderDescription
HOSTNAMEYour regional gRPC host, e.g. grpc-staging-eu.kodypay.com (staging) or grpc-eu.kodypay.com (live)
API_KEYYour Kody API key
STORE_IDYour Kody store identifier
TERMINAL_IDYour terminal serial number

Endpoints

#EndpointgRPC callDescription
1List of TerminalsTerminalsList your terminals and their online status.
2Initiate Terminal PaymentPaySend a payment to a terminal (server-streaming).
3Cancel Terminal PaymentCancelCancel a payment in progress on a terminal.
4Get Terminal Payment DetailsPaymentDetailsRetrieve a terminal payment by order ID.
5Refund PaymentRefundRefund via the backend or a terminal (server-streaming).
6Void PaymentVoidVoid a processed payment before settlement.
7Close BatchCloseBatchClose payment batches for one or all terminals.

All calls are methods on KodyPayTerminalService.

Common enums

enum PaymentStatus {
PENDING = 0;
SUCCESS = 1;
FAILED = 2;
CANCELLED = 3;
DECLINED = 4;
REFUND_PENDING = 5;
REFUND_REQUESTED = 6;
}

1. List of Terminals

Method: KodyPayTerminalService.Terminals

Retrieves a list of all terminals associated with your store, along with their online status.

Request

rpc Terminals(TerminalsRequest) returns (TerminalsResponse);

message TerminalsRequest {
string store_id = 1; // UUID of store
}

Request fields

FieldTypeRequiredDescription
store_idstringYesUUID of the store.

Response

message TerminalsResponse {
repeated Terminal terminals = 1;
}

message Terminal {
string terminal_id = 1; // Terminal serial number
bool online = 2; // Online status
}

Examples

Show code examples (Java · Python · .NET · PHP)
import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc;
import com.kodypay.grpc.pay.v1.TerminalsRequest;
import com.kodypay.grpc.pay.v1.TerminalsResponse;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;

public class ListTerminalsExample {
public static final String HOSTNAME = "HOSTNAME";
public static final String API_KEY = "API_KEY";

public static void main(String[] args) {
Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("X-API-Key", Metadata.ASCII_STRING_MARSHALLER), API_KEY);

var channel = ManagedChannelBuilder.forAddress(HOSTNAME, 443)
.useTransportSecurity()
.build();
var client = KodyPayTerminalServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));

TerminalsRequest request = TerminalsRequest.newBuilder()
.setStoreId("STORE_ID")
.build();

TerminalsResponse response = client.terminals(request);
response.getTerminalsList().forEach(terminal -> {
System.out.println("Terminal ID: " + terminal.getTerminalId());
System.out.println("Online: " + terminal.getOnline());
});
}
}

2. Initiate Terminal Payment

Method: KodyPayTerminalService.Pay

Sends a payment request to a terminal. The terminal displays the payment screen (and, optionally, tip options) for an in-person transaction.

Request

rpc Pay(PayRequest) returns (stream PayResponse);

message PayRequest {
string store_id = 1; // UUID of store
string amount = 2; // Amount in BigDecimal/2.dp (e.g., "10.00")
string terminal_id = 3; // Terminal serial number
optional bool show_tips = 4; // Flag to display tips on the terminal
optional PaymentMethod payment_method = 5; // Specific payment method; if unset, the terminal will prompt the customer
optional string idempotency_uuid = 6; // UUID idempotency key (generated by client)
optional string payment_reference = 7; // Unique payment reference provided by client
optional string order_id = 8; // Unique order reference provided by client
repeated PayRequest.PaymentMethods accepts_only = 9; // Inclusion list of accepted payment methods. It is optional. If not specified, all payment methods enabled for the store will be accepted.
}

enum PaymentMethods {
VISA = 0;
MASTERCARD = 1;
AMEX = 2;
BAN_CONTACT = 3;
CHINA_UNION_PAY = 4;
MAESTRO = 5;
DINERS = 6;
DISCOVER = 7;
JCB = 8;
ALIPAY = 9;
WECHAT = 10;
}

message PaymentMethod {
PaymentMethodType payment_method_type = 1; // e.g., CARD, E_WALLET
oneof verification_mode {
string token = 2; // With a token provided, QR scanning is skipped and the terminal goes straight to the payment screen.
bool activate_qr_code_scanner = 3; // Set to true to activate the terminal camera to scan a customer's or Kody's QR Code; false (default) displays a QR code for the customer to scan.
}
}

enum PaymentMethodType {
CARD = 0;
E_WALLET = 1;
UNKNOWN = 2; // Response only — indicates an unrecognised payment method
}

Request fields

FieldTypeRequiredDescription
store_idstringYesUUID of the store.
amountstringYesAmount as a 2dp decimal string (e.g. "10.00").
terminal_idstringYesTerminal serial number.
show_tipsboolNoDisplay tip options on the terminal.
payment_methodPaymentMethodNoSpecific method; if unset, the terminal prompts the customer.
idempotency_uuidstringNoClient-generated idempotency key.
payment_referencestringNoYour unique payment reference.
order_idstringNoYour unique order reference.
accepts_onlyPaymentMethods[]NoAllow-list of accepted methods; defaults to all enabled.

Response

message PayResponse {
PaymentStatus status = 1;
optional string failure_reason = 2; // Populated on failure
string payment_id = 4;
google.protobuf.Timestamp date_created = 5;
optional PaymentData payment_data = 11;
optional string payment_reference = 12;
optional string order_id = 13;
repeated PayRequest.PaymentMethods accepts_only = 14;
optional bool is_payment_declined = 15; // true if the payment was declined rather than cancelled
repeated RefundDetails refunds = 16;

message PaymentData {
google.protobuf.Timestamp date_paid = 1;
string total_amount = 2;
string sale_amount = 3;
string tips_amount = 4;
string receipt_json = 5; // Receipt details in JSON format
string psp_reference = 6;
PaymentMethodType payment_method_type = 7;
string payment_method = 8; // Payment method. e.g., visa, mc, amex, etc.
optional PaymentCard payment_card = 9; // Deprecated.
optional string payment_method_variant = 10; // Payment method variant. e.g., mc, mcsuperpremiumcredit, visa, visadebit, visacredit, etc.
optional string original_currency = 12; // The currency that applies to total_amount, sale_amount, and tips_amount. Defined by the store’s configured settlement currency.
optional string paid_amount = 13; // The amount paid by the customer. This will differ from total_amount if the customer pays in their home currency.
optional string paid_currency = 14; // The currency in which the customer paid. This will differ from original_currency if the customer pays in their home currency.
optional string exchange_rate = 15; // The exchange rate between original_currency and paid_currency, calculated as total_amount / paid_amount. Precision: up to 6 decimal places.
optional string batch_id = 16; // The identifier of the payment batch on the terminal in which this payment was created. Applicable only to batch payments.

message PaymentCard {
string card_last_4_digits = 1;
string card_expiry_date = 2;
string pos_entry_mode = 3;
string payment_token = 4; // Deprecated — use the Tokenised Payments API instead.
string auth_code = 5;
optional string card_bin = 6; // The card’s Bank Identification Number (BIN), used to identify the issuing bank. Corresponds to the first 6 digits of the card number.
optional string card_issuing_bank = 7; // Issuing bank name, e.g., STANDARD CHARTERED BANK (HONG KONG) LTD
optional string funding_source = 8; // The funding source of the customer’s card, e.g., CREDIT, DEBIT, or PREPAID.
}
}

message RefundDetails {
string payment_id = 1;
optional string refund_psp_reference = 2; // PSP reference; may not be available yet if the refund is still pending
string payment_transaction_id = 3;
string refund_amount = 4;
google.protobuf.Timestamp event_date = 5;
optional string terminal_id = 6;
}
}

Examples

Show code examples (Java · Python · .NET · PHP)
package com.kody;

import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc;
import com.kodypay.grpc.pay.v1.PayRequest;
import com.kodypay.grpc.pay.v1.PayResponse;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;

import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;

public class InitiateTerminalPaymentExample {
public static final String HOSTNAME = "HOSTNAME";
public static final String API_KEY = "API_KEY";

public static void main(String[] args) {
// Replace with your store and terminal IDs
String storeId = "STORE_ID";
String terminalId = "TERMINAL_ID";
BigDecimal amount = new BigDecimal("10.00");

initiatePayment(storeId, terminalId, amount);
}

private static void initiatePayment(String storeId, String terminalId, BigDecimal amount) {
var client = createTerminalClient();

PayRequest request = PayRequest.newBuilder()
.setStoreId(storeId)
.setAmount(amount.toString())
.setTerminalId(terminalId)
.setShowTips(true)
// Optionally set payment_method, idempotency_uuid, payment_reference, order_id, etc.
.build();

// Since Pay returns a stream, get the first response from the iterator
PayResponse response = client.pay(request).next();

System.out.println("Payment ID: " + response.getPaymentId());
System.out.println("Status: " + response.getStatus());
// Additional fields (e.g. payment_data) can be inspected as needed
}

private static KodyPayTerminalServiceGrpc.KodyPayTerminalServiceBlockingStub createTerminalClient() {
Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("X-API-Key", Metadata.ASCII_STRING_MARSHALLER), API_KEY);

return KodyPayTerminalServiceGrpc.newBlockingStub(ManagedChannelBuilder
.forAddress(HOSTNAME, 443)
.idleTimeout(3, TimeUnit.MINUTES)
.keepAliveTimeout(3, TimeUnit.MINUTES)
.intercept(MetadataUtils.newAttachHeadersInterceptor(metadata))
.build());
}
}

3. Cancel Terminal Payment

Method: KodyPayTerminalService.Cancel

Cancels a payment that is in progress on a terminal.

Request

rpc Cancel(CancelRequest) returns (CancelResponse);

message CancelRequest {
string store_id = 1; // UUID of store
string amount = 2; // Amount in BigDecimal/2.dp (must match the original request)
string terminal_id = 3; // Terminal serial number where the payment was sent
optional string payment_id = 4; // (Optional) Payment ID (order) to cancel
}

Request fields

FieldTypeRequiredDescription
store_idstringYesUUID of the store.
amountstringYesAmount as a 2dp decimal; must match the original request.
terminal_idstringYesTerminal serial number the payment was sent to.
payment_idstringNoPayment (order) ID to cancel.

Response

message CancelResponse {
PaymentStatus status = 1; // Status of the payment after cancellation attempt, should be CANCELLED if the cancellation is successful.
}

Examples

Show code examples (Java · Python · .NET · PHP)
package com.kody;

import com.kodypay.grpc.pay.v1.CancelRequest;
import com.kodypay.grpc.pay.v1.CancelResponse;
import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;

import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;

public class CancelTerminalPaymentExample {
public static final String HOSTNAME = "HOSTNAME";
public static final String API_KEY = "API_KEY";

public static void main(String[] args) {
String storeId = "STORE_ID";
String terminalId = "TERMINAL_ID";
String paymentId = "PAYMENT_ID"; // Optional: include if available
BigDecimal amount = new BigDecimal("10.00");

cancelPayment(storeId, terminalId, paymentId, amount);
}

private static void cancelPayment(String storeId, String terminalId, String paymentId, BigDecimal amount) {
var client = createTerminalClient();
CancelRequest.Builder requestBuilder = CancelRequest.newBuilder()
.setStoreId(storeId)
.setTerminalId(terminalId)
.setAmount(amount.toString());
if (paymentId != null && !paymentId.isEmpty()) {
requestBuilder.setPaymentId(paymentId);
}

CancelResponse response = client.cancel(requestBuilder.build());
System.out.println("Cancel Status: " + response.getStatus());
}

private static KodyPayTerminalServiceGrpc.KodyPayTerminalServiceBlockingStub createTerminalClient() {
Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("X-API-Key", Metadata.ASCII_STRING_MARSHALLER), API_KEY);
return KodyPayTerminalServiceGrpc.newBlockingStub(ManagedChannelBuilder
.forAddress(HOSTNAME, 443)
.idleTimeout(3, TimeUnit.MINUTES)
.keepAliveTimeout(3, TimeUnit.MINUTES)
.intercept(MetadataUtils.newAttachHeadersInterceptor(metadata))
.build());
}
}

4. Get Terminal Payment Details

Method: KodyPayTerminalService.PaymentDetails

Retrieves the details of a specific terminal payment. Identify the payment by passing the payment_id returned by the Pay response in the order_id field.

note

The order_id field must be set to the payment_id returned by the Pay response — not a client-supplied order reference.

Request

rpc PaymentDetails(PaymentDetailsRequest) returns (PayResponse);

message PaymentDetailsRequest {
string store_id = 1; // UUID of store
string order_id = 2; // Set to the payment_id returned by the Pay response
}

Request fields

FieldTypeRequiredDescription
store_idstringYesUUID of the store.
order_idstringYesThe payment_id returned by the Pay response (passed in the order_id field).

Response

message PayResponse {
PaymentStatus status = 1;
optional string failure_reason = 2; // Populated on failure
string payment_id = 4;
google.protobuf.Timestamp date_created = 5;
optional PaymentData payment_data = 11;
optional string payment_reference = 12;
optional string order_id = 13;
repeated PayRequest.PaymentMethods accepts_only = 14;
optional bool is_payment_declined = 15; // true if the payment was declined rather than cancelled
repeated RefundDetails refunds = 16;

message PaymentData {
google.protobuf.Timestamp date_paid = 1;
string total_amount = 2;
string sale_amount = 3;
string tips_amount = 4;
string receipt_json = 5; // Receipt details in JSON format
string psp_reference = 6;
PaymentMethodType payment_method_type = 7;
string payment_method = 8; // Payment method. e.g., visa, mc, amex, etc.
optional PaymentCard payment_card = 9; // Deprecated.
optional string payment_method_variant = 10; // Payment method variant. e.g., mc, mcsuperpremiumcredit, visa, visadebit, visacredit, etc.
optional string original_currency = 12; // The currency that applies to total_amount, sale_amount, and tips_amount. Defined by the store’s configured settlement currency.
optional string paid_amount = 13; // The amount paid by the customer. This will differ from total_amount if the customer pays in their home currency.
optional string paid_currency = 14; // The currency in which the customer paid. This will differ from original_currency if the customer pays in their home currency.
optional string exchange_rate = 15; // The exchange rate between original_currency and paid_currency, calculated as total_amount / paid_amount. Precision: up to 6 decimal places.
optional string batch_id = 16; // The identifier of the payment batch on the terminal in which this payment was created. Applicable only to batch payments.

message PaymentCard {
string card_last_4_digits = 1;
string card_expiry_date = 2;
string pos_entry_mode = 3;
string payment_token = 4; // Deprecated — use the Tokenised Payments API instead.
string auth_code = 5;
optional string card_bin = 6; // The card's Bank Identification Number (BIN), used to identify the issuing bank. Corresponds to the first 6 digits of the card number.
optional string card_issuing_bank = 7; // Issuing bank name, e.g., STANDARD CHARTERED BANK (HONG KONG) LTD
optional string funding_source = 8; // The funding source of the customer's card, e.g., CREDIT, DEBIT, or PREPAID.
}
}

message RefundDetails {
string payment_id = 1;
optional string refund_psp_reference = 2; // PSP reference; may not be available yet if the refund is still pending
string payment_transaction_id = 3;
string refund_amount = 4;
google.protobuf.Timestamp event_date = 5;
optional string terminal_id = 6;
}
}

Examples

Show code examples (Java · Python · .NET · PHP)
package com.kody;

import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc;
import com.kodypay.grpc.pay.v1.PaymentDetailsRequest;
import com.kodypay.grpc.pay.v1.PayResponse;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;

import java.util.Date;
import java.util.concurrent.TimeUnit;

public class GetPaymentDetailsExample {
public static final String HOSTNAME = "HOSTNAME";
public static final String API_KEY = "API_KEY";

public static void main(String[] args) {
String storeId = "STORE_ID";
// Use the payment_id returned by the Pay response
String orderId = "PAYMENT_ID";

getPaymentDetails(storeId, orderId);
}

private static void getPaymentDetails(String storeId, String orderId) {
var client = createTerminalClient();

PaymentDetailsRequest request = PaymentDetailsRequest.newBuilder()
.setStoreId(storeId)
.setOrderId(orderId)
.build();

PayResponse response = client.paymentDetails(request);

System.out.println("Payment ID: " + response.getPaymentId());
System.out.println("Status: " + response.getStatus());
System.out.println("Order ID: " + response.getOrderId());
System.out.println("Created: " + new Date(response.getDateCreated().getSeconds() * 1000L));
// Inspect additional fields as needed (e.g., receipt_json, payment_data, etc.)
}

private static KodyPayTerminalServiceGrpc.KodyPayTerminalServiceBlockingStub createTerminalClient() {
Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("X-API-Key", Metadata.ASCII_STRING_MARSHALLER), API_KEY);
return KodyPayTerminalServiceGrpc.newBlockingStub(ManagedChannelBuilder
.forAddress(HOSTNAME, 443)
.idleTimeout(3, TimeUnit.MINUTES)
.keepAliveTimeout(3, TimeUnit.MINUTES)
.intercept(MetadataUtils.newAttachHeadersInterceptor(metadata))
.build());
}
}

5. Refund Payment

Method: KodyPayTerminalService.Refund

There are two ways to perform refunds:

  1. Backend Refund — Initiate a refund through the backend system. This method does not require a physical terminal.
  2. Terminal Refund — Initiate a refund through Kody's physical payment terminal. The terminal must be online at the time of the request. This method enables printing refund receipts directly from the terminal.

Refunds are initiated as a stream of responses.

Two-step payments (TSP)

For two-step payments (TSP), a payment can only be refunded after its batch has been closed. Close the batch first (see Close Batch), then initiate the refund.

Request

rpc Refund(RefundRequest) returns (stream RefundResponse);

message RefundRequest {
string store_id = 1; // UUID of store
string payment_id = 2; // Payment ID to refund
string amount = 3; // Refund amount (BigDecimal/2.dp, e.g., "5.00")
optional string idempotency_uuid = 4; // UUID idempotency key
optional string terminal_id = 5; // Terminal ID for processing terminal-based refunds
optional string ext_pay_reference = 6;
optional string ext_order_id = 7;
}

Request fields

FieldTypeRequiredDescription
store_idstringYesUUID of the store.
payment_idstringYesPayment ID to refund.
amountstringYesRefund amount as a 2dp decimal string (e.g. "5.00").
idempotency_uuidstringNoClient-generated idempotency key.
terminal_idstringNoTerminal ID; include to process the refund on a terminal.
ext_pay_referencestringNoYour external payment reference.
ext_order_idstringNoYour external order ID.

Response

message RefundResponse {
RefundStatus status = 1;
optional string failure_reason = 2;
string payment_id = 3;
google.protobuf.Timestamp date_created = 4;
string total_paid_amount = 5;
string total_amount_refunded = 6;
string remaining_amount = 7;
string total_amount_requested = 8;
string paymentTransactionId = 9;
enum RefundStatus {
PENDING = 0;
REQUESTED = 1;
FAILED = 2;
}
optional string order_id = 10;
optional string ext_pay_reference = 11;
optional string ext_order_id = 12;
}

5.1 Backend Refund

Initiates a refund through the backend system without requiring a physical terminal. This is useful when a terminal is not available or needed.

Key Points

  • No terminal required — The refund is processed entirely through the backend
  • Omit terminal_id — Simply do not include the terminal_id field in your request
  • Immediate processing — The refund is processed immediately and the result is returned in the response

Examples

Show code examples (Java · Python · .NET · PHP)
package com.kody;

import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc;
import com.kodypay.grpc.pay.v1.RefundRequest;
import com.kodypay.grpc.pay.v1.RefundResponse;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;

import java.util.Iterator;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class BackendRefundExample {
public static final String HOSTNAME = "HOSTNAME";
public static final String API_KEY = "API_KEY";

public static void main(String[] args) {
String storeId = "STORE_ID";
String paymentId = "PAYMENT_ID"; // Payment to refund
String refundAmount = "5.00";
String idempotencyKey = UUID.randomUUID().toString();

ManagedChannel channel = ManagedChannelBuilder.forAddress(HOSTNAME, 443)
.useTransportSecurity()
.build();

Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("X-API-Key", Metadata.ASCII_STRING_MARSHALLER), API_KEY);
KodyPayTerminalServiceGrpc.KodyPayTerminalServiceBlockingStub client =
KodyPayTerminalServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));

// Create a backend refund request (no terminal_id specified)
RefundRequest refundRequest = RefundRequest.newBuilder()
.setStoreId(storeId)
.setPaymentId(paymentId)
.setAmount(refundAmount)
.setIdempotencyUuid(idempotencyKey)
.build();

Iterator<RefundResponse> responses = client.refund(refundRequest);
while (responses.hasNext()) {
RefundResponse response = responses.next();
System.out.println("Backend Refund Response for Payment ID: " + response.getPaymentId());
System.out.println("Status: " + response.getStatus());
if (response.getStatus() == RefundResponse.RefundStatus.FAILED) {
System.out.println("Failure Reason: " + response.getFailureReason());
}
System.out.println("Total Amount Refunded: " + response.getTotalAmountRefunded());
}

channel.shutdownNow();
try {
channel.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

5.2 Terminal Refund

Initiates a refund through a physical payment terminal. The terminal must be online and ready to process the refund request.

Key Points

  • Terminal must be online — The specified terminal must be turned on and ready to accept the refund request
  • Include terminal_id — Specify which terminal should process the refund
  • Any terminal in the store — You can use any terminal belonging to the same store to process the refund, not just the one that processed the original payment
  • Hands-free terminal — The refund is processed without requiring any interaction at the terminal
  • Receipt printing — The terminal will print a receipt according to the terminal's configured receipt settings

Examples

Show code examples (Java · Python · .NET · PHP)
package com.kody;

import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc;
import com.kodypay.grpc.pay.v1.RefundRequest;
import com.kodypay.grpc.pay.v1.RefundResponse;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;

import java.util.Iterator;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class TerminalRefundExample {
public static final String HOSTNAME = "HOSTNAME";
public static final String API_KEY = "API_KEY";

public static void main(String[] args) {
String storeId = "STORE_ID";
String paymentId = "PAYMENT_ID"; // Payment to refund
String terminalId = "TERMINAL_ID"; // Terminal to process the refund
String refundAmount = "5.00";
String idempotencyKey = UUID.randomUUID().toString();

ManagedChannel channel = ManagedChannelBuilder.forAddress(HOSTNAME, 443)
.useTransportSecurity()
.build();

Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("X-API-Key", Metadata.ASCII_STRING_MARSHALLER), API_KEY);
KodyPayTerminalServiceGrpc.KodyPayTerminalServiceBlockingStub client =
KodyPayTerminalServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));

// Create a terminal refund request (with terminal_id specified)
RefundRequest refundRequest = RefundRequest.newBuilder()
.setStoreId(storeId)
.setPaymentId(paymentId)
.setAmount(refundAmount)
.setIdempotencyUuid(idempotencyKey)
.setTerminalId(terminalId)
.build();

Iterator<RefundResponse> responses = client.refund(refundRequest);
while (responses.hasNext()) {
RefundResponse response = responses.next();
System.out.println("Terminal Refund Response for Payment ID: " + response.getPaymentId());
System.out.println("Status: " + response.getStatus());
if (response.getStatus() == RefundResponse.RefundStatus.FAILED) {
System.out.println("Failure Reason: " + response.getFailureReason());
}
System.out.println("Total Amount Refunded: " + response.getTotalAmountRefunded());
}

channel.shutdownNow();
try {
channel.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

5.3 Cross-Store Refund Support

Kody supports cross-store refunds, allowing you to process a refund from a different store than where the original payment occurred, provided both stores are within the same merchant account.

With this feature, a POS terminal in Store A can initiate a refund for a transaction that originally occurred in Store B, provided that the API key used has access to the relevant store.

Key Points

  • Cross-store capability — Refunds can be performed from a different store than where the original payment occurred.
  • API key permissions — The X-API-Key must have access rights to the store where the original transaction was processed.
  • Same API interface — Cross-store refunds use the exact same request format and endpoints as same-store refunds. No changes to your integration are needed.
  • Flexible refund origin — You may use either backend or terminal-based refunds for cross-store scenarios.

6. Void Payment

Method: KodyPayTerminalService.Void

Reverses a processed payment before it settles to the merchant account.

Request

rpc Void(VoidPaymentRequest) returns (VoidPaymentResponse);

message VoidPaymentRequest {
oneof ids {
string psp_reference = 1;
string payment_id = 2;
}
optional string payment_reference = 3;
optional string order_id = 4;
string store_id = 5;
}

Request fields

FieldTypeRequiredDescription
psp_referencestringOne ofPSP reference of the payment to void.
payment_idstringOne ofKody payment ID of the payment to void.
payment_referencestringNoYour payment reference.
order_idstringNoYour order ID.
store_idstringYesUUID of the store.

Response

message VoidPaymentResponse {
string psp_reference = 1;
string payment_id = 2;
VoidStatus status = 3;
optional SaleData sale_data = 4;
google.protobuf.Timestamp date_voided = 5;

enum VoidStatus {
PENDING = 0;
REQUESTED = 1;
VOIDED = 2;
FAILED = 3;
}

message SaleData {
string total_amount = 1;
string sale_amount = 2;
string tips_amount = 3;
string order_id = 4;
string payment_reference = 5;
}
}

Examples

Show code examples (Java · Python · .NET · PHP)
package com.kody;

import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc;
import com.kodypay.grpc.pay.v1.VoidPaymentRequest;
import com.kodypay.grpc.pay.v1.VoidPaymentResponse;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;

import java.util.concurrent.TimeUnit;

public class VoidPaymentExample {
public static final String HOSTNAME = "HOSTNAME";
public static final String API_KEY = "API_KEY";

public static void main(String[] args) {
String storeId = "STORE_ID";
// Void by payment_id (alternatively, you can use psp_reference)
String paymentId = "PAYMENT_ID";

ManagedChannel channel = ManagedChannelBuilder.forAddress(HOSTNAME, 443)
.useTransportSecurity()
.build();

Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("X-API-Key", Metadata.ASCII_STRING_MARSHALLER), API_KEY);
KodyPayTerminalServiceGrpc.KodyPayTerminalServiceBlockingStub client =
KodyPayTerminalServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));

VoidPaymentRequest voidRequest = VoidPaymentRequest.newBuilder()
.setStoreId(storeId)
.setPaymentId(paymentId)
.build();

// Note: `void` is a reserved word in Java, so the generated stub method is `void_()`
VoidPaymentResponse response = client.void_(voidRequest);
System.out.println("Void Payment Response:");
System.out.println("Payment ID: " + response.getPaymentId());
System.out.println("PSP Reference: " + response.getPspReference());
System.out.println("Status: " + response.getStatus());
if (response.hasSaleData()) {
System.out.println("Sale Total Amount: " + response.getSaleData().getTotalAmount());
}
System.out.println("Date Voided: " + response.getDateVoided());

channel.shutdownNow();
try {
channel.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

7. Close Batch

Method: KodyPayTerminalService.CloseBatch

Closes payment batches for one or all terminals in a store. You can target specific terminals or, by omitting terminal IDs, every terminal in the store.

Request

rpc CloseBatch(CloseBatchRequest) returns (CloseBatchResponse);

message CloseBatchRequest {
string store_id = 1; // Required: Store identifier
repeated string terminal_ids = 2; // Optional: Terminal identifiers. If empty, closes batches for all terminals in the store
}

Request fields

FieldTypeRequiredDescription
store_idstringYesStore identifier.
terminal_idsstring[]NoTerminals to close; if empty, closes all in the store.

Response

message CloseBatchResponse {
CloseBatchStatus status = 1; // Overall status: "success", "partial_success", "failure"
string message = 2; // Human-readable summary of the closure operation, e.g., "Successfully initiated closure for 6 batch(es)"
repeated CloseBatchResult results = 3; // Results for each batch closure attempt

enum CloseBatchStatus {
SUCCESS = 0; // All batch closures initiated successfully
PARTIAL_SUCCESS = 1; // Some batches initiated closure successfully, others failed
FAILURE = 2; // All batch closure attempts failed
}
}

message CloseBatchResult {
string terminal_id = 1; // Terminal that owned the batch
int32 batch_id = 2; // Batch identifier
bool success = 3; // Whether this closure initiation was successful
string message = 4; // Success or error message for this batch
}

Examples

Show code examples (Java · Python · .NET · PHP)
import com.kodypay.grpc.pay.v1.KodyPayTerminalServiceGrpc;
import com.kodypay.grpc.pay.v1.CloseBatchRequest;
import com.kodypay.grpc.pay.v1.CloseBatchResponse;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;

public class CloseBatchExample {
public static final String HOSTNAME = "HOSTNAME";
public static final String API_KEY = "API_KEY";

public static void main(String[] args) {
// Replace with your store ID
String storeId = "STORE_ID";

Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("X-API-Key", Metadata.ASCII_STRING_MARSHALLER), API_KEY);

var channel = ManagedChannelBuilder.forAddress(HOSTNAME, 443)
.useTransportSecurity()
.build();
var client = KodyPayTerminalServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));

// Close batches for all terminals in the store
CloseBatchRequest request = CloseBatchRequest.newBuilder()
.setStoreId(storeId)
.build();

// Or close batch for specific terminal(s)
// CloseBatchRequest request = CloseBatchRequest.newBuilder()
// .setStoreId(storeId)
// .addTerminalIds("TERMINAL_ID_1")
// .addTerminalIds("TERMINAL_ID_2")
// .build();

CloseBatchResponse response = client.closeBatch(request);

System.out.println("Overall Status: " + response.getStatus());
System.out.println("Message: " + response.getMessage());

for (var result : response.getResultsList()) {
System.out.println("Terminal ID: " + result.getTerminalId());
System.out.println("Batch ID: " + result.getBatchId());
System.out.println("Success: " + result.getSuccess());
System.out.println("Message: " + result.getMessage());
}
}
}

Need help?

For further support or more detailed information, contact the Kody Support team at support@kody.com.