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.
| Region | Development and Test | Live |
|---|---|---|
| Asia-Pacific | grpc-staging-ap.kodypay.com | grpc-ap.kodypay.com |
| Europe | grpc-staging-eu.kodypay.com | grpc-eu.kodypay.com |
Use the regional staging host that matches your store in the examples below. Replace the following placeholders with your own values:
| Placeholder | Description |
|---|---|
HOSTNAME | Your regional gRPC host, e.g. grpc-staging-eu.kodypay.com (staging) or grpc-eu.kodypay.com (live) |
API_KEY | Your Kody API key |
STORE_ID | Your Kody store identifier |
TERMINAL_ID | Your terminal serial number |
Endpoints
| # | Endpoint | gRPC call | Description |
|---|---|---|---|
| 1 | List of Terminals | Terminals | List your terminals and their online status. |
| 2 | Initiate Terminal Payment | Pay | Send a payment to a terminal (server-streaming). |
| 3 | Cancel Terminal Payment | Cancel | Cancel a payment in progress on a terminal. |
| 4 | Get Terminal Payment Details | PaymentDetails | Retrieve a terminal payment by order ID. |
| 5 | Refund Payment | Refund | Refund via the backend or a terminal (server-streaming). |
| 6 | Void Payment | Void | Void a processed payment before settlement. |
| 7 | Close Batch | CloseBatch | Close 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.7A 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
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | UUID 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)
- 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());
});
}
}
import grpc
import kody_clientsdk_python.pay.v1.pay_pb2 as kody_model
import kody_clientsdk_python.pay.v1.pay_pb2_grpc as kody_client
def list_terminals():
channel = grpc.secure_channel("HOSTNAME:443", grpc.ssl_channel_credentials())
client = kody_client.KodyPayTerminalServiceStub(channel)
metadata = [("x-api-key", "API_KEY")]
request = kody_model.TerminalsRequest(store_id="STORE_ID")
response = client.Terminals(request, metadata=metadata)
for terminal in response.terminals:
print(f"Terminal ID: {terminal.terminal_id}")
print(f"Online: {terminal.online}")
if __name__ == "__main__":
list_terminals()
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Pay.V1;
class Program
{
static async Task Main(string[] args)
{
var HOSTNAME = "HOSTNAME";
var API_KEY = "API_KEY";
var channel = GrpcChannel.ForAddress("https://" + HOSTNAME);
var client = new KodyPayTerminalService.KodyPayTerminalServiceClient(channel);
var metadata = new Metadata
{
{ "X-API-Key", API_KEY }
};
var request = new TerminalsRequest { StoreId = "STORE_ID" };
var response = await client.TerminalsAsync(request, metadata);
foreach (var terminal in response.Terminals)
{
Console.WriteLine($"Terminal ID: {terminal.TerminalId}");
Console.WriteLine($"Online: {terminal.Online}");
}
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Pay\V1\KodyPayTerminalServiceClient;
use Com\Kodypay\Pay\V1\TerminalsRequest;
use Grpc\ChannelCredentials;
$HOSTNAME = "HOSTNAME";
$API_KEY = "API_KEY";
$client = new KodyPayTerminalServiceClient($HOSTNAME, [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => [$API_KEY]];
$request = new TerminalsRequest();
$request->setStoreId('STORE_ID');
list($response, $status) = $client->Terminals($request, $metadata)->wait();
if ($status->code !== \Grpc\STATUS_OK) {
echo "Error: " . $status->details . PHP_EOL;
} else {
foreach ($response->getTerminals() as $terminal) {
echo "Terminal ID: " . $terminal->getTerminalId() . PHP_EOL;
echo "Online: " . ($terminal->getOnline() ? 'Yes' : 'No') . PHP_EOL;
}
}
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
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | UUID of the store. |
amount | string | Yes | Amount as a 2dp decimal string (e.g. "10.00"). |
terminal_id | string | Yes | Terminal serial number. |
show_tips | bool | No | Display tip options on the terminal. |
payment_method | PaymentMethod | No | Specific method; if unset, the terminal prompts the customer. |
idempotency_uuid | string | No | Client-generated idempotency key. |
payment_reference | string | No | Your unique payment reference. |
order_id | string | No | Your unique order reference. |
accepts_only | PaymentMethods[] | No | Allow-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)
- 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());
}
}
from datetime import datetime
import grpc
import kody_clientsdk_python.pay.v1.pay_pb2 as kody_model
import kody_clientsdk_python.pay.v1.pay_pb2_grpc as kody_client
def initiate_terminal_payment():
hostname = "HOSTNAME:443"
api_key = "API_KEY"
store_id = "STORE_ID"
terminal_id = "TERMINAL_ID"
amount = "10.00"
# Optional: define a PaymentMethod, for example:
payment_method = kody_model.PaymentMethod(
payment_method_type=kody_model.PaymentMethodType.E_WALLET,
activate_qr_code_scanner=True
)
request = kody_model.PayRequest(
store_id=store_id,
amount=amount,
terminal_id=terminal_id,
show_tips=True,
payment_method=payment_method
)
channel = grpc.secure_channel(hostname, grpc.ssl_channel_credentials())
client = kody_client.KodyPayTerminalServiceStub(channel)
metadata = [("x-api-key", api_key)]
response_iterator = client.Pay(request, metadata=metadata)
# Process the first response from the stream
for response in response_iterator:
print(f"Payment ID: {response.payment_id}")
print(f"Status: {kody_model.PaymentStatus.Name(response.status)}")
if response.failure_reason:
print(f"Failure Reason: {response.failure_reason}")
break
if __name__ == "__main__":
initiate_terminal_payment()
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Pay.V1;
class Program
{
static async Task Main(string[] args)
{
var HOSTNAME = "HOSTNAME";
var API_KEY = "API_KEY";
var channel = GrpcChannel.ForAddress("https://" + HOSTNAME);
var client = new KodyPayTerminalService.KodyPayTerminalServiceClient(channel);
var metadata = new Metadata { { "X-API-Key", API_KEY } };
var request = new PayRequest
{
StoreId = "STORE_ID",
Amount = "10.00",
TerminalId = "TERMINAL_ID",
ShowTips = true
// Optionally set PaymentMethod, IdempotencyUuid, PaymentReference, OrderId, etc.
};
using var call = client.Pay(request, metadata);
if (await call.ResponseStream.MoveNext())
{
var response = call.ResponseStream.Current;
Console.WriteLine($"Payment ID: {response.PaymentId}");
Console.WriteLine($"Status: {response.Status}");
}
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Pay\V1\KodyPayTerminalServiceClient;
use Com\Kodypay\Pay\V1\PayRequest;
use Grpc\ChannelCredentials;
$HOSTNAME = "HOSTNAME";
$API_KEY = "API_KEY";
$client = new KodyPayTerminalServiceClient($HOSTNAME, [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => [$API_KEY]];
$request = new PayRequest();
$request->setStoreId('STORE_ID');
$request->setAmount("10.00");
$request->setTerminalId('TERMINAL_ID');
$request->setShowTips(true);
// Optionally set payment_method, idempotency_uuid, payment_reference, order_id, etc.
$call = $client->Pay($request, $metadata);
foreach ($call->responses() as $response) {
echo "Payment ID: " . $response->getPaymentId() . PHP_EOL;
echo "Status: " . $response->getStatus() . PHP_EOL;
break; // Process only the first response
}
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
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | UUID of the store. |
amount | string | Yes | Amount as a 2dp decimal; must match the original request. |
terminal_id | string | Yes | Terminal serial number the payment was sent to. |
payment_id | string | No | Payment (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)
- 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());
}
}
import grpc
import kody_clientsdk_python.pay.v1.pay_pb2 as kody_model
import kody_clientsdk_python.pay.v1.pay_pb2_grpc as kody_client
def cancel_terminal_payment():
hostname = "HOSTNAME:443"
api_key = "API_KEY"
store_id = "STORE_ID"
terminal_id = "TERMINAL_ID"
amount = "10.00"
# Optionally, set payment_id if available:
payment_id = "PAYMENT_ID" # or leave as empty string if not available
request = kody_model.CancelRequest(
store_id=store_id,
terminal_id=terminal_id,
amount=amount,
)
if payment_id:
request.payment_id = payment_id
channel = grpc.secure_channel(hostname, grpc.ssl_channel_credentials())
client = kody_client.KodyPayTerminalServiceStub(channel)
metadata = [("x-api-key", api_key)]
response = client.Cancel(request, metadata=metadata)
print(f"Cancel Status: {response.status}")
if __name__ == "__main__":
cancel_terminal_payment()
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Pay.V1;
class CancelTerminalPaymentExample
{
public static async Task Main(string[] args)
{
var HOSTNAME = "HOSTNAME";
var API_KEY = "API_KEY";
// Create a secure channel to the host
var channel = GrpcChannel.ForAddress("https://" + HOSTNAME);
var client = new KodyPayTerminalService.KodyPayTerminalServiceClient(channel);
// Add API key to metadata
var metadata = new Metadata { { "X-API-Key", API_KEY } };
// Build the cancel request
var request = new CancelRequest
{
StoreId = "STORE_ID",
TerminalId = "TERMINAL_ID",
Amount = "10.00"
};
// Optionally, set PaymentId if available
request.PaymentId = "PAYMENT_ID";
// Send the cancel request asynchronously and await the response
var response = await client.CancelAsync(request, metadata);
Console.WriteLine($"Cancel Status: {response.Status}");
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Pay\V1\KodyPayTerminalServiceClient;
use Com\Kodypay\Pay\V1\CancelRequest;
use Grpc\ChannelCredentials;
$HOSTNAME = "HOSTNAME";
$API_KEY = "API_KEY";
$client = new KodyPayTerminalServiceClient($HOSTNAME, [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => [$API_KEY]];
$request = new CancelRequest();
$request->setStoreId('STORE_ID');
$request->setTerminalId('TERMINAL_ID');
$request->setAmount("10.00");
// Optionally set payment_id if available:
// $request->setPaymentId('PAYMENT_ID');
list($response, $status) = $client->Cancel($request, $metadata)->wait();
if ($status->code !== \Grpc\STATUS_OK) {
echo "Error: " . $status->details . PHP_EOL;
} else {
echo "Cancel Status: " . $response->getStatus() . PHP_EOL;
}
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.
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
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | UUID of the store. |
order_id | string | Yes | The 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 out | Read |
|---|---|
| Whether any money has been returned | amount_refunded |
| Whether the payment is fully refunded | fully_refunded |
| What happened to an individual refund | refunds[].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.
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)
- 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());
}
}
from datetime import datetime
import grpc
import kody_clientsdk_python.pay.v1.pay_pb2 as kody_model
import kody_clientsdk_python.pay.v1.pay_pb2_grpc as kody_client
def get_terminal_payment_details():
hostname = "HOSTNAME:443"
api_key = "API_KEY"
store_id = "STORE_ID"
order_id = "PAYMENT_ID" # the payment_id returned by the Pay response
channel = grpc.secure_channel(hostname, grpc.ssl_channel_credentials())
client = kody_client.KodyPayTerminalServiceStub(channel)
metadata = [("x-api-key", api_key)]
request = kody_model.PaymentDetailsRequest(store_id=store_id, order_id=order_id)
response = client.PaymentDetails(request, metadata=metadata)
print(f"Payment ID: {response.payment_id}")
print(f"Status: {kody_model.PaymentStatus.Name(response.status)}")
print(f"Order ID: {response.order_id}")
print(f"Created: {datetime.fromtimestamp(response.date_created.seconds)}")
if response.failure_reason:
print(f"Failure Reason: {response.failure_reason}")
if __name__ == "__main__":
get_terminal_payment_details()
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Pay.V1;
class Program
{
static async Task Main(string[] args)
{
var HOSTNAME = "HOSTNAME";
var API_KEY = "API_KEY";
var channel = GrpcChannel.ForAddress("https://" + HOSTNAME);
var client = new KodyPayTerminalService.KodyPayTerminalServiceClient(channel);
var metadata = new Metadata { { "X-API-Key", API_KEY } };
var request = new PaymentDetailsRequest
{
StoreId = "STORE_ID",
OrderId = "PAYMENT_ID" // the payment_id returned by the Pay response
};
var response = await client.PaymentDetailsAsync(request, metadata);
Console.WriteLine($"Payment ID: {response.PaymentId}");
Console.WriteLine($"Status: {response.Status}");
Console.WriteLine($"Order ID: {response.OrderId}");
Console.WriteLine($"Created: {response.DateCreated.ToDateTime():g}");
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Pay\V1\KodyPayTerminalServiceClient;
use Com\Kodypay\Pay\V1\PaymentDetailsRequest;
use Grpc\ChannelCredentials;
$HOSTNAME = "HOSTNAME";
$API_KEY = "API_KEY";
$client = new KodyPayTerminalServiceClient($HOSTNAME, [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => [$API_KEY]];
$request = new PaymentDetailsRequest();
$request->setStoreId('STORE_ID');
$request->setOrderId('PAYMENT_ID'); // the payment_id returned by the Pay response
list($response, $status) = $client->PaymentDetails($request, $metadata)->wait();
if ($status->code !== \Grpc\STATUS_OK) {
echo "Error: " . $status->details . PHP_EOL;
} else {
echo "Payment ID: " . $response->getPaymentId() . PHP_EOL;
echo "Status: " . $response->getStatus() . PHP_EOL;
echo "Order ID: " . $response->getOrderId() . PHP_EOL;
}
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.
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
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | UUID of the store. |
payment_id | string | Yes | Payment ID to refund. |
amount | string | Yes | Refund amount as a 2dp decimal string (e.g. "5.00"). |
idempotency_uuid | string | No | Client-generated idempotency key. |
terminal_id | string | No | Terminal ID; include to process the refund on a terminal. |
ext_pay_reference | string | No | Your external payment reference. |
ext_order_id | string | No | Your 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_idfield 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)
- 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();
}
}
}
import grpc
import uuid
from kody_clientsdk_python.pay.v1 import pay_pb2 as kody_model
from kody_clientsdk_python.pay.v1 import pay_pb2_grpc as kody_client
def backend_refund_payment():
hostname = "HOSTNAME:443"
api_key = "API_KEY"
store_id = "STORE_ID"
payment_id = "PAYMENT_ID" # Payment to refund
refund_amount = "5.00"
idempotency_uuid = str(uuid.uuid4())
channel = grpc.secure_channel(hostname, grpc.ssl_channel_credentials())
client = kody_client.KodyPayTerminalServiceStub(channel)
metadata = [("x-api-key", api_key)]
# Create a backend refund request (no terminal_id specified)
refund_request = kody_model.RefundRequest(
store_id=store_id,
payment_id=payment_id,
amount=refund_amount,
idempotency_uuid=idempotency_uuid
)
responses = client.Refund(refund_request, metadata=metadata)
for response in responses:
print(f"Backend Refund Response for Payment ID: {response.payment_id}")
print(f"Status: {kody_model.RefundResponse.RefundStatus.Name(response.status)}")
if response.status == kody_model.RefundResponse.RefundStatus.FAILED:
print(f"Failure Reason: {response.failure_reason}")
print(f"Total Amount Refunded: {response.total_amount_refunded}")
if __name__ == "__main__":
backend_refund_payment()
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Pay.V1;
class BackendRefundExample
{
public static async Task Main(string[] args)
{
string hostname = "https://HOSTNAME";
string apiKey = "API_KEY";
string storeId = "STORE_ID";
string paymentId = "PAYMENT_ID"; // Payment to refund
string refundAmount = "5.00";
string idempotencyUuid = Guid.NewGuid().ToString();
using var channel = GrpcChannel.ForAddress(hostname);
var client = new KodyPayTerminalService.KodyPayTerminalServiceClient(channel);
var metadata = new Metadata { { "X-API-Key", apiKey } };
// Create a backend refund request (no terminal_id specified)
var refundRequest = new RefundRequest
{
StoreId = storeId,
PaymentId = paymentId,
Amount = refundAmount,
IdempotencyUuid = idempotencyUuid
};
using var call = client.Refund(refundRequest, metadata);
while (await call.ResponseStream.MoveNext())
{
var response = call.ResponseStream.Current;
Console.WriteLine($"Backend Refund Response for Payment ID: {response.PaymentId}");
Console.WriteLine($"Status: {response.Status}");
if (response.Status == RefundResponse.Types.RefundStatus.Failed)
{
Console.WriteLine($"Failure Reason: {response.FailureReason}");
}
Console.WriteLine($"Total Amount Refunded: {response.TotalAmountRefunded}");
}
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Pay\V1\KodyPayTerminalServiceClient;
use Com\Kodypay\Pay\V1\RefundRequest;
use Grpc\ChannelCredentials;
$HOSTNAME = "HOSTNAME";
$API_KEY = "API_KEY";
$client = new KodyPayTerminalServiceClient($HOSTNAME, [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => [$API_KEY]];
// Create a backend refund request (no terminal_id specified)
$refundRequest = new RefundRequest();
$refundRequest->setStoreId('STORE_ID');
$refundRequest->setPaymentId('PAYMENT_ID'); // Payment to refund
$refundRequest->setAmount("5.00");
// Optionally, set an idempotency UUID:
$refundRequest->setIdempotencyUuid(uuid_create(UUID_TYPE_RANDOM));
$call = $client->Refund($refundRequest, $metadata);
foreach ($call->responses() as $response) {
echo "Backend Refund Response for Payment ID: " . $response->getPaymentId() . PHP_EOL;
echo "Status: " . $response->getStatus() . PHP_EOL;
if ($response->getStatus() === \Com\Kodypay\Pay\V1\RefundResponse\RefundStatus::FAILED) {
echo "Failure Reason: " . $response->getFailureReason() . PHP_EOL;
}
echo "Total Amount Refunded: " . $response->getTotalAmountRefunded() . PHP_EOL;
}
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)
- 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();
}
}
}
import grpc
import uuid
from kody_clientsdk_python.pay.v1 import pay_pb2 as kody_model
from kody_clientsdk_python.pay.v1 import pay_pb2_grpc as kody_client
def terminal_refund_payment():
hostname = "HOSTNAME:443"
api_key = "API_KEY"
store_id = "STORE_ID"
payment_id = "PAYMENT_ID" # Payment to refund
terminal_id = "TERMINAL_ID" # Terminal to process the refund
refund_amount = "5.00"
idempotency_uuid = str(uuid.uuid4())
channel = grpc.secure_channel(hostname, grpc.ssl_channel_credentials())
client = kody_client.KodyPayTerminalServiceStub(channel)
metadata = [("x-api-key", api_key)]
# Create a terminal refund request (with terminal_id specified)
refund_request = kody_model.RefundRequest(
store_id=store_id,
payment_id=payment_id,
amount=refund_amount,
idempotency_uuid=idempotency_uuid,
terminal_id=terminal_id
)
responses = client.Refund(refund_request, metadata=metadata)
for response in responses:
print(f"Terminal Refund Response for Payment ID: {response.payment_id}")
print(f"Status: {kody_model.RefundResponse.RefundStatus.Name(response.status)}")
if response.status == kody_model.RefundResponse.RefundStatus.FAILED:
print(f"Failure Reason: {response.failure_reason}")
print(f"Total Amount Refunded: {response.total_amount_refunded}")
if __name__ == "__main__":
terminal_refund_payment()
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Pay.V1;
class TerminalRefundExample
{
public static async Task Main(string[] args)
{
string hostname = "https://HOSTNAME";
string apiKey = "API_KEY";
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 idempotencyUuid = Guid.NewGuid().ToString();
using var channel = GrpcChannel.ForAddress(hostname);
var client = new KodyPayTerminalService.KodyPayTerminalServiceClient(channel);
var metadata = new Metadata { { "X-API-Key", apiKey } };
// Create a terminal refund request (with terminal_id specified)
var refundRequest = new RefundRequest
{
StoreId = storeId,
PaymentId = paymentId,
Amount = refundAmount,
IdempotencyUuid = idempotencyUuid,
TerminalId = terminalId
};
using var call = client.Refund(refundRequest, metadata);
while (await call.ResponseStream.MoveNext())
{
var response = call.ResponseStream.Current;
Console.WriteLine($"Terminal Refund Response for Payment ID: {response.PaymentId}");
Console.WriteLine($"Status: {response.Status}");
if (response.Status == RefundResponse.Types.RefundStatus.Failed)
{
Console.WriteLine($"Failure Reason: {response.FailureReason}");
}
Console.WriteLine($"Total Amount Refunded: {response.TotalAmountRefunded}");
}
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Pay\V1\KodyPayTerminalServiceClient;
use Com\Kodypay\Pay\V1\RefundRequest;
use Grpc\ChannelCredentials;
$HOSTNAME = "HOSTNAME";
$API_KEY = "API_KEY";
$client = new KodyPayTerminalServiceClient($HOSTNAME, [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => [$API_KEY]];
// Create a terminal refund request (with terminal_id specified)
$refundRequest = new RefundRequest();
$refundRequest->setStoreId('STORE_ID');
$refundRequest->setPaymentId('PAYMENT_ID'); // Payment to refund
$refundRequest->setAmount("5.00");
$refundRequest->setTerminalId('TERMINAL_ID'); // Terminal to process the refund
// Optionally, set an idempotency UUID:
$refundRequest->setIdempotencyUuid(uuid_create(UUID_TYPE_RANDOM));
$call = $client->Refund($refundRequest, $metadata);
foreach ($call->responses() as $response) {
echo "Terminal Refund Response for Payment ID: " . $response->getPaymentId() . PHP_EOL;
echo "Status: " . $response->getStatus() . PHP_EOL;
if ($response->getStatus() === \Com\Kodypay\Pay\V1\RefundResponse\RefundStatus::FAILED) {
echo "Failure Reason: " . $response->getFailureReason() . PHP_EOL;
}
echo "Total Amount Refunded: " . $response->getTotalAmountRefunded() . PHP_EOL;
}
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 removedAs 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
| Field | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | Kody payment ID of the payment to void. |
payment_reference | string | No | Your payment reference. |
order_id | string | No | Your order ID. |
store_id | string | Yes | UUID 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)
- 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();
}
}
}
import grpc
from kody_clientsdk_python.pay.v1 import pay_pb2 as kody_model
from kody_clientsdk_python.pay.v1 import pay_pb2_grpc as kody_client
def void_payment():
hostname = "HOSTNAME:443"
api_key = "API_KEY"
store_id = "STORE_ID"
payment_id = "PAYMENT_ID"
channel = grpc.secure_channel(hostname, grpc.ssl_channel_credentials())
client = kody_client.KodyPayTerminalServiceStub(channel)
metadata = [("x-api-key", "API_KEY")]
void_request = kody_model.VoidPaymentRequest(
store_id=store_id,
payment_id=payment_id
# Optionally, set payment_reference and order_id if needed
)
response = client.Void(void_request, metadata=metadata)
print("Void Payment Response:")
print(f"Payment ID: {response.payment_id}")
print(f"PSP Reference: {response.psp_reference}")
print(f"Status: {response.status}")
if response.HasField("sale_data"):
print(f"Sale Total Amount: {response.sale_data.total_amount}")
print(f"Date Voided: {response.date_voided}")
if __name__ == "__main__":
void_payment()
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Pay.V1;
class VoidPaymentExample
{
public static async Task Main(string[] args)
{
string hostname = "https://HOSTNAME";
string apiKey = "API_KEY";
string storeId = "STORE_ID";
string paymentId = "PAYMENT_ID";
var channel = GrpcChannel.ForAddress(hostname);
var client = new KodyPayTerminalService.KodyPayTerminalServiceClient(channel);
var metadata = new Metadata { { "X-API-Key", apiKey } };
var voidRequest = new VoidPaymentRequest
{
StoreId = storeId,
PaymentId = paymentId
// Optionally, set PaymentReference and OrderId if needed
};
var response = await client.VoidAsync(voidRequest, metadata);
Console.WriteLine("Void Payment Response:");
Console.WriteLine($"Payment ID: {response.PaymentId}");
Console.WriteLine($"PSP Reference: {response.PspReference}");
Console.WriteLine($"Status: {response.Status}");
if (response.SaleData != null)
{
Console.WriteLine($"Sale Total Amount: {response.SaleData.TotalAmount}");
}
Console.WriteLine($"Date Voided: {response.DateVoided.ToDateTime():g}");
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Pay\V1\KodyPayTerminalServiceClient;
use Com\Kodypay\Pay\V1\VoidPaymentRequest;
use Grpc\ChannelCredentials;
$HOSTNAME = "HOSTNAME";
$API_KEY = "API_KEY";
$client = new KodyPayTerminalServiceClient($HOSTNAME, [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => [$API_KEY]];
$voidRequest = new VoidPaymentRequest();
$voidRequest->setStoreId('STORE_ID');
$voidRequest->setPaymentId('PAYMENT_ID');
list($response, $status) = $client->Void($voidRequest, $metadata)->wait();
if ($status->code !== \Grpc\STATUS_OK) {
echo "Error: " . $status->details . PHP_EOL;
} else {
echo "Void Payment Response:" . PHP_EOL;
echo "Payment ID: " . $response->getPaymentId() . PHP_EOL;
echo "PSP Reference: " . $response->getPspReference() . PHP_EOL;
echo "Status: " . $response->getStatus() . PHP_EOL;
if ($response->getSaleData()) {
echo "Sale Total Amount: " . $response->getSaleData()->getTotalAmount() . PHP_EOL;
}
echo "Date Voided: " . $response->getDateVoided() . PHP_EOL;
}
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
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | Store identifier. |
terminal_ids | string[] | No | Terminals 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)
- 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());
}
}
}
import grpc
import kody_clientsdk_python.pay.v1.pay_pb2 as kody_model
import kody_clientsdk_python.pay.v1.pay_pb2_grpc as kody_client
def close_batch():
hostname = "HOSTNAME:443"
api_key = "API_KEY"
store_id = "STORE_ID"
channel = grpc.secure_channel(hostname, grpc.ssl_channel_credentials())
client = kody_client.KodyPayTerminalServiceStub(channel)
metadata = [("x-api-key", api_key)]
# Close batches for all terminals in the store
request = kody_model.CloseBatchRequest(store_id=store_id)
# Or close batch for specific terminal(s)
# request = kody_model.CloseBatchRequest(
# store_id=store_id,
# terminal_ids=["TERMINAL_ID_1", "TERMINAL_ID_2"]
# )
response = client.CloseBatch(request, metadata=metadata)
print(f"Overall Status: {response.status}")
print(f"Message: {response.message}")
for result in response.results:
print(f"Terminal ID: {result.terminal_id}")
print(f"Batch ID: {result.batch_id}")
print(f"Success: {result.success}")
print(f"Message: {result.message}")
if __name__ == "__main__":
close_batch()
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Pay.V1;
class Program
{
static async Task Main(string[] args)
{
var HOSTNAME = "HOSTNAME";
var API_KEY = "API_KEY";
var channel = GrpcChannel.ForAddress("https://" + HOSTNAME);
var client = new KodyPayTerminalService.KodyPayTerminalServiceClient(channel);
var metadata = new Metadata
{
{ "X-API-Key", API_KEY }
};
// Close batches for all terminals in the store
var request = new CloseBatchRequest { StoreId = "STORE_ID" };
// Or close batch for specific terminal(s)
// var request = new CloseBatchRequest
// {
// StoreId = "STORE_ID"
// };
// request.TerminalIds.Add("TERMINAL_ID_1");
// request.TerminalIds.Add("TERMINAL_ID_2");
var response = await client.CloseBatchAsync(request, metadata);
Console.WriteLine($"Overall Status: {response.Status}");
Console.WriteLine($"Message: {response.Message}");
foreach (var result in response.Results)
{
Console.WriteLine($"Terminal ID: {result.TerminalId}");
Console.WriteLine($"Batch ID: {result.BatchId}");
Console.WriteLine($"Success: {result.Success}");
Console.WriteLine($"Message: {result.Message}");
}
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Pay\V1\KodyPayTerminalServiceClient;
use Com\Kodypay\Pay\V1\CloseBatchRequest;
use Grpc\ChannelCredentials;
$HOSTNAME = "HOSTNAME";
$API_KEY = "API_KEY";
$client = new KodyPayTerminalServiceClient($HOSTNAME, [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => [$API_KEY]];
// Close batches for all terminals in the store
$request = new CloseBatchRequest();
$request->setStoreId('STORE_ID');
// Or close batch for specific terminal(s)
// $request->setTerminalIds(['TERMINAL_ID_1', 'TERMINAL_ID_2']);
list($response, $status) = $client->CloseBatch($request, $metadata)->wait();
if ($status->code !== \Grpc\STATUS_OK) {
echo "Error: " . $status->details . PHP_EOL;
} else {
echo "Overall Status: " . $response->getStatus() . PHP_EOL;
echo "Message: " . $response->getMessage() . PHP_EOL;
foreach ($response->getResults() as $result) {
echo "Terminal ID: " . $result->getTerminalId() . PHP_EOL;
echo "Batch ID: " . $result->getBatchId() . PHP_EOL;
echo "Success: " . ($result->getSuccess() ? 'Yes' : 'No') . PHP_EOL;
echo "Message: " . $result->getMessage() . PHP_EOL;
}
}
Need help?
For further support or more detailed information, contact the Kody Support team at support@kody.com.