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 [deprecated = true]; // Use PayResponse.refunds[].status
REFUND_REQUESTED = 6 [deprecated = true]; // Use PayResponse.refunds[].status
}

// A refund's own lifecycle, which is not the payment's: the payment stays successful throughout.
message Refund {
enum Status {
UNSPECIFIED = 0; // Never sent deliberately; a legal fallback rather than an illegal one
PENDING = 1; // Not yet sent to the acquirer
REQUESTED = 2; // The acquirer accepted the refund request. Not yet money back
SUCCEEDED = 3; // The refund completed
FAILED = 4; // The refund did not happen and will not
CANCELLED = 5; // The refund was cancelled before it completed
}
}
Refund.Status — available from protocol v1.8.7

A refund now reports its own status on PayResponse.refunds[].status. PaymentStatus.REFUND_PENDING and REFUND_REQUESTED are deprecated as of the same version: they describe the payment with a refund's state, which cannot express a refund that failed, and a payment with two refunds in different states has no single correct value. They are still populated — nothing breaks — but new integrations should read refunds[].status.

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;
// Refund aggregates — available from protocol v1.8.7
optional string amount_refunded = 18; // Refunded so far, counting only refunds that succeeded. BigDecimal/2.dp
optional bool fully_refunded = 19; // True only when the whole payment has been refunded

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;
optional PaymentMethodType payment_method_type = 7; // Explicit presence: CARD is the zero value, so check presence before reading.
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; // Absent until the acquirer has accepted the refund
string payment_transaction_id = 3; // This refund's own id. Use it to identify this refund
string refund_amount = 4;
google.protobuf.Timestamp event_date = 5;
optional string terminal_id = 6;
Refund.Status status = 7; // What this refund is doing — available from protocol v1.8.7
}
}

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;
// Refund aggregates — available from protocol v1.8.7
optional string amount_refunded = 18; // Refunded so far, counting only refunds that succeeded. BigDecimal/2.dp
optional bool fully_refunded = 19; // True only when the whole payment has been refunded

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;
optional PaymentMethodType payment_method_type = 7; // Explicit presence: CARD is the zero value, so check presence before reading.
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; // Absent until the acquirer has accepted the refund
string payment_transaction_id = 3; // This refund's own id. Use it to identify this refund
string refund_amount = 4;
google.protobuf.Timestamp event_date = 5;
optional string terminal_id = 6;
Refund.Status status = 7; // What this refund is doing — available from protocol v1.8.7
}
}

Reading the refund fields

Refunds are asynchronous: the acquirer confirms the outcome after the refund request has returned. These fields are how you observe that outcome — poll this endpoint until each refund reaches a terminal status.

To find outRead
Whether any money has been returnedamount_refunded
Whether the payment is fully refundedfully_refunded
What happened to an individual refundrefunds[].status

amount_refunded counts only refunds that succeeded. A refund that is still pending, or that failed, has moved no money and is not included — so the total never overstates what the payer has received back. It is derived from the rows in refunds, so the two cannot disagree.

fully_refunded is measured against the charged amount, sale plus tip. Both aggregates are absent on a payment with no payment attempt yet, where the question does not apply.

Branch on status. Do not infer the outcome from whether refund_psp_reference is set: that tells you the acquirer accepted the request, not that the money has moved, and it is absent until then.

Refund fields — available from protocol v1.8.7

amount_refunded, fully_refunded and RefundDetails.status were added in protocol v1.8.7. See Protocol versions and SDK releases for what that means for your language.

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

Issues a refund for a specific terminal payment, either in full or in part. Set terminal_id to print a refund receipt from the terminal; omit it to process the refund without one. 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();
}
}
}

6. Void Payment

Method: KodyPayTerminalService.Void

Reverses a processed payment before it settles to the merchant account. Identify the payment with its payment_id.

psp_reference removed

As of 2026-09-05, voiding by psp_reference is no longer supported. A request that still sets it now carries no identifier and is rejected rather than voiding anything. Identify the payment with payment_id.

Request

rpc Void(VoidPaymentRequest) returns (VoidPaymentResponse);

message VoidPaymentRequest {
// Voiding by psp_reference is no longer supported: removed 2026-09-05 after 90 days with no request
// using it. A request that still sets it now carries no identifier and is rejected rather than
// voiding anything. Field 1 and the name stay reserved so neither can be reused for something else.
reserved 1;
reserved "psp_reference";
// Kept as a oneof with one member: exactly one identifier, and room for another later without
// changing the shape of the generated code.
oneof ids {
string payment_id = 2;
}
optional string payment_reference = 3 ;
optional string order_id = 4;
string store_id = 5;
}

Request fields

FieldTypeRequiredDescription
payment_idstringYesKody 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";
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 {
optional CloseBatchStatus status = 1; // Overall status: "success", "partial_success", "failure". Explicit presence: SUCCESS is the zero value, so check presence before reading.
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.