Online Payments
The Online Payments API lets you take card and wallet payments for e-commerce checkouts. Use it to initiate a payment and redirect the customer to a hosted payment page, track the payment to completion, list past payments, and issue refunds. It also supports payer details, card tokenisation, expiry, and delayed capture/release.
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 |
Endpoints
| # | Endpoint | gRPC call | Description |
|---|---|---|---|
| 1 | Initiate Payment | InitiatePayment | Create a payment and return a hosted payment URL. |
| 2 | Initiate Payment Stream | InitiatePaymentStream | Create a payment and stream status until it completes. |
| 3 | Payment Details | PaymentDetails | Fetch the current status and details of a payment. |
| 4 | Payment Details Stream | PaymentDetailsStream | Stream a payment's details until it completes. |
| 5 | Get Payments | GetPayments | List payments with pagination and filters. |
| 6 | Refund Payment | Refund | Refund all or part of a payment. |
All calls are methods on KodyEcomPaymentsService.
Payment flow
Common enums
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;
}
enum PaymentStatus {
PENDING = 0;
SUCCESS = 1;
FAILED = 2;
CANCELLED = 3;
EXPIRED = 4;
}
// 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
}
}
PaymentStatus does not change when a refund is takenA fully refunded payment still reports SUCCESS. A refund is a separate object with its own
lifecycle, reported on PaymentDetails.refunds[].status using Refund.Status above.
Refund.Status is available from protocol v1.8.7.
1. Initiate Payment
Method: KodyEcomPaymentsService.InitiatePayment
Initiates an online payment and returns a payment_url for the customer to complete the transaction. After payment, the customer is redirected to the specified return_url.
Request
rpc InitiatePayment(PaymentInitiationRequest) returns (PaymentInitiationResponse);
message PaymentInitiationRequest {
string store_id = 1; // Your Kody store id
string payment_reference = 2; // Your unique reference for this payment request.
uint64 amount_minor_units = 3; // Amount in minor units. For example, 2000 means GBP 20.00.
string currency = 4; // ISO 4217 three letter currency code (e.g., GBP, HKD), must be the same as store currency
string order_id = 5; // Your identifier for the order. (May be reused if the same order has multiple payments)
optional string order_metadata = 6; // Additional order data (e.g., JSON with checkout items)
string return_url = 7; // URL to redirect after the payment is authorised
optional string payer_statement = 8; // Text for the payer's bank statement. Max 22 characters, otherwise banks might truncate the string. If not set, uses the store's terminal receipt printing name. Allowed characters: a-z, A-Z, 0-9, spaces, and special characters . , ' _ - ? + * /
optional string payer_email_address = 9; // Payer's email address (recommended for fraud and 3D Secure 2 checks)
optional string payer_ip_address = 10; // Payer's IP address (for risk analysis)
optional string payer_locale = 11; // Locale (e.g., en and zh) to display the payment pages in the desired language
optional bool tokenise_card = 12; // Flag to tokenise the card; defaults to false
optional ExpirySettings expiry = 13; // Settings for payment expiry
message ExpirySettings {
bool show_timer = 1; // Display a countdown timer on the payment page (default false)
uint64 expiring_seconds = 2; // Timeout duration in seconds (default: 1800 seconds)
}
}
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | Your Kody store ID. |
payment_reference | string | Yes | Your unique reference for this payment request. |
amount_minor_units | uint64 | Yes | Amount in minor units (e.g. 2000 = GBP 20.00). |
currency | string | Yes | ISO 4217 currency code; must match the store currency. |
order_id | string | Yes | Your order identifier (may be reused across payments). |
order_metadata | string | No | Additional order data (e.g. JSON of checkout items). |
return_url | string | Yes | URL to redirect to after the payment is authorised. |
payer_statement | string | No | Text for the payer's bank statement. Max 22 characters — longer values risk truncation by the bank. If not set, uses the store's terminal receipt printing name. Allowed characters: a-z, A-Z, 0-9, spaces, and . , ' _ - ? + * /. |
payer_email_address | string | No | Payer email; recommended for fraud and 3DS2 checks. |
payer_ip_address | string | No | Payer IP address, used for risk analysis. |
payer_locale | string | No | Locale for the payment pages (e.g. en, zh). |
tokenise_card | bool | No | Tokenise the card for reuse. Defaults to false. |
expiry | ExpirySettings | No | Payment expiry / countdown timer settings. |
Expiry settings (expiry)
| Field | Type | Required | Description |
|---|---|---|---|
show_timer | bool | No | Display a countdown timer on the payment page. Defaults to false. |
expiring_seconds | uint64 | No | Timeout duration in seconds. Defaults to 1800. |
Response
message PaymentInitiationResponse {
oneof result {
Response response = 1;
Error error = 2;
}
message Response {
string payment_id = 1; // The unique identifier created by Kody
string payment_url = 2; // URL for the customer to complete the payment
}
message Error {
Type type = 1;
string message = 2;
enum Type {
UNKNOWN = 0;
DUPLICATE_ATTEMPT = 1;
INVALID_REQUEST = 2;
}
}
}
Examples
Show code examples (Java · Python · .NET · PHP)
- Java
- Python
- .NET
- PHP
package com.kody;
import com.kodypay.grpc.ecom.v1.KodyEcomPaymentsServiceGrpc;
import com.kodypay.grpc.ecom.v1.PaymentInitiationRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;
import java.util.UUID;
public class InitiateEcomPaymentExample {
public static final String HOSTNAME = "HOSTNAME";
public static final String API_KEY = "API_KEY";
public static void main(String[] args) {
String storeId = "STORE_ID";
initiateEcomPayment(storeId);
}
public static void initiateEcomPayment(String storeId) {
// Create a managed channel and attach the API key metadata.
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);
var paymentClient = KodyEcomPaymentsServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
String orderId = "order_" + UUID.randomUUID();
String paymentReference = "pay_" + UUID.randomUUID();
PaymentInitiationRequest request = PaymentInitiationRequest.newBuilder()
.setStoreId(storeId)
.setPaymentReference(paymentReference)
.setAmountMinorUnits(1000) // e.g., 1000 for £10.00
.setCurrency("GBP")
.setOrderId(orderId)
.setReturnUrl("https://your-return-url.com")
// Optionally, set additional fields:
.setPayerEmailAddress("customer@example.com")
.build();
var response = paymentClient.initiatePayment(request);
if (response.hasResponse()) {
System.out.println("Payment URL: " + response.getResponse().getPaymentUrl());
System.out.println("Payment ID: " + response.getResponse().getPaymentId());
} else {
System.err.println("Error: " + response.getError().getMessage());
}
channel.shutdownNow();
}
}
import grpc
from uuid import uuid4
import kody_clientsdk_python.ecom.v1.ecom_pb2 as kody_model
import kody_clientsdk_python.ecom.v1.ecom_pb2_grpc as kody_client
def initiate_payment():
hostname = "HOSTNAME"
api_key = "API_KEY"
store_id = "STORE_ID"
with grpc.secure_channel(hostname, grpc.ssl_channel_credentials()) as channel:
client = kody_client.KodyEcomPaymentsServiceStub(channel)
request = kody_model.PaymentInitiationRequest(
store_id=store_id,
payment_reference=f"pay_{uuid4()}",
amount_minor_units=1000, # e.g., 1000 for £10.00
currency="GBP",
order_id=f"order_{uuid4()}",
return_url="https://your-return-url.com",
payer_email_address="customer@example.com" # Optional
)
response = client.InitiatePayment(request, metadata=[("x-api-key", api_key)])
if response.HasField("response"):
print(f"Payment ID: {response.response.payment_id}")
print(f"Payment URL: {response.response.payment_url}")
else:
print(f"Error: {response.error.message}")
if __name__ == "__main__":
initiate_payment()
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Ecom.V1;
class Program
{
static async Task Main()
{
string hostname = "https://HOSTNAME";
string apiKey = "API_KEY";
var channel = GrpcChannel.ForAddress(hostname);
var client = new KodyEcomPaymentsService.KodyEcomPaymentsServiceClient(channel);
var metadata = new Metadata { { "X-API-Key", apiKey } };
var request = new PaymentInitiationRequest
{
StoreId = "STORE_ID",
PaymentReference = $"pay_{Guid.NewGuid()}",
AmountMinorUnits = 1000, // e.g., 1000 for £10.00
Currency = "GBP",
OrderId = $"order_{Guid.NewGuid()}",
ReturnUrl = "https://your-return-url.com",
PayerEmailAddress = "customer@example.com" // Optional
};
var response = await client.InitiatePaymentAsync(request, metadata);
if (response.ResultCase == PaymentInitiationResponse.ResultOneofCase.Response)
{
Console.WriteLine($"Payment ID: {response.Response.PaymentId}");
Console.WriteLine($"Payment URL: {response.Response.PaymentUrl}");
}
else
{
Console.WriteLine($"Error: {response.Error.Message}");
}
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Ecom\V1\KodyEcomPaymentsServiceClient;
use Com\Kodypay\Ecom\V1\PaymentInitiationRequest;
use Grpc\ChannelCredentials;
$client = new KodyEcomPaymentsServiceClient('HOSTNAME:443', [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => ['API_KEY']];
$request = new PaymentInitiationRequest();
$request->setStoreId('STORE_ID');
$request->setPaymentReference('pay_' . uniqid());
$request->setAmountMinorUnits(1000); // e.g., 1000 for £10.00
$request->setCurrency('GBP');
$request->setOrderId('order_' . uniqid());
$request->setReturnUrl('https://your-return-url.com');
// Optionally, set additional fields:
$request->setPayerEmailAddress("customer@example.com");
list($response, $status) = $client->InitiatePayment($request, $metadata)->wait();
if ($status->code !== \Grpc\STATUS_OK) {
echo "Error: " . $status->details . PHP_EOL;
} else if ($response->getResponse()) {
echo "Payment ID: " . $response->getResponse()->getPaymentId() . PHP_EOL;
echo "Payment URL: " . $response->getResponse()->getPaymentUrl() . PHP_EOL;
} else {
echo "Error: " . $response->getError()->getMessage() . PHP_EOL;
}
2. Initiate Payment Stream
Method: KodyEcomPaymentsService.InitiatePaymentStream
Initiates an online payment and streams status updates until the transaction completes. The first (interim) message carries the hosted payment page link at payment_data.payment_wallet.payment_link_id — redirect the customer there to pay. The stream reuses PaymentDetailsResponse, so there is no top-level payment_url field. After payment, the customer is redirected to the specified return_url.
Request
rpc InitiatePaymentStream(PaymentInitiationRequest) returns (stream PaymentDetailsResponse);
message PaymentInitiationRequest {
string store_id = 1; // Your Kody store id
string payment_reference = 2; // Your unique reference for this payment request.
uint64 amount_minor_units = 3; // Amount in minor units. For example, 2000 means GBP 20.00.
string currency = 4; // ISO 4217 three letter currency code (e.g., GBP, HKD)
string order_id = 5; // Your identifier for the order. (May be reused if the same order has multiple payments)
optional string order_metadata = 6; // Additional order data (e.g., JSON with checkout items)
string return_url = 7; // URL to redirect after the payment is authorised
optional string payer_statement = 8; // Text for the payer's bank statement. Max 22 characters, otherwise banks might truncate the string. If not set, uses the store's terminal receipt printing name. Allowed characters: a-z, A-Z, 0-9, spaces, and special characters . , ' _ - ? + * /
optional string payer_email_address = 9; // Payer's email address (recommended for fraud and 3D Secure 2 checks)
optional string payer_ip_address = 10; // Payer's IP address (for risk analysis)
optional string payer_locale = 11; // Locale (e.g., en, zh) to display the payment pages in the desired language
optional bool tokenise_card = 12; // Flag to tokenise the card; defaults to false
optional ExpirySettings expiry = 13; // Settings for payment expiry
message ExpirySettings {
bool show_timer = 1; // Display a countdown timer on the payment page (default false)
uint64 expiring_seconds = 2; // Timeout duration in seconds (default: 1800 seconds)
}
}
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | Your Kody store ID. |
payment_reference | string | Yes | Your unique reference for this payment request. |
amount_minor_units | uint64 | Yes | Amount in minor units (e.g. 2000 = GBP 20.00). |
currency | string | Yes | ISO 4217 currency code. |
order_id | string | Yes | Your order identifier (may be reused across payments). |
order_metadata | string | No | Additional order data (e.g. JSON of checkout items). |
return_url | string | Yes | URL to redirect to after the payment is authorised. |
payer_statement | string | No | Text for the payer's bank statement. Max 22 characters — longer values risk truncation by the bank. If not set, uses the store's terminal receipt printing name. Allowed characters: a-z, A-Z, 0-9, spaces, and . , ' _ - ? + * /. |
payer_email_address | string | No | Payer email; recommended for fraud and 3DS2 checks. |
payer_ip_address | string | No | Payer IP address, used for risk analysis. |
payer_locale | string | No | Locale for the payment pages (e.g. en, zh). |
tokenise_card | bool | No | Tokenise the card for reuse. Defaults to false. |
expiry | ExpirySettings | No | Payment expiry / countdown timer settings. |
Response
message PaymentDetailsResponse {
oneof result {
PaymentDetails response = 1;
Error error = 2;
}
message PaymentDetails {
string payment_id = 1; // Kody-generated payment identifier
PaymentStatus status = 5;
google.protobuf.Timestamp date_created = 7;
// Detailed payment data (if available)
optional PaymentData payment_data = 11;
// Sale-related data (if applicable)
optional SaleData sale_data = 12;
// Refund fields — available from protocol v1.8.7
repeated RefundDetails refunds = 13; // Every refund taken against this payment, oldest first
uint64 amount_refunded_minor_units = 14; // Refunded so far, counting only refunds that succeeded
bool fully_refunded = 15; // True only when the whole payment has been refunded
}
// One refund taken against the payment. Available from protocol v1.8.7.
message RefundDetails {
string payment_transaction_id = 1; // This refund's own id. Use it to identify this refund
optional string refund_psp_reference = 2; // Absent until the acquirer has accepted the refund
string refund_amount = 3; // BigDecimal/2.dp (e.g. "10.00")
google.protobuf.Timestamp event_date = 4;
Refund.Status status = 5; // What this refund is doing — see Common enums
}
message Error {
Type type = 1;
string message = 2;
enum Type {
UNKNOWN = 0;
NOT_FOUND = 1;
INVALID_REQUEST = 2;
}
}
}
// PaymentData contains detailed information about the payment method and authorisation
message PaymentData {
string psp_reference = 1; // Payment service provider reference
optional PaymentMethods payment_method = 2; // Payment method (VISA, MASTERCARD, etc.). Explicit presence: VISA is the zero value, so check presence before reading.
string payment_method_variant = 3; // Variant of the payment method
PaymentAuthStatus auth_status = 4; // Authorisation status
google.protobuf.Timestamp auth_status_date = 5; // Date/time of the auth status change
enum PaymentAuthStatus {
PENDING = 0;
AUTHORISED = 1;
FAILED = 2;
CAPTURED = 3;
RELEASED = 4;
EXPIRED = 5;
}
oneof payment_method_details {
PaymentCard payment_card = 6; // Card payment details
PaymentWallet payment_wallet = 7; // Digital wallet payment details
}
message PaymentCard {
string card_last_4_digits = 1; // Last 4 digits of the card
string auth_code = 2; // Authorisation code
string payment_token = 3; // Card token (if tokenisation was requested)
}
message PaymentWallet {
optional string card_last_4_digits = 1; // Last 4 digits (if available)
string payment_link_id = 2; // Wallet payment link identifier
}
}
// SaleData contains information about the sale/order
message SaleData {
uint64 amount_minor_units = 1; // Amount in minor units (e.g., 2000 for £20.00)
string currency = 2; // ISO 4217 currency code
string order_id = 3; // Your order identifier
string payment_reference = 4; // Your payment reference
optional string order_metadata = 5; // Additional order data
}
Examples
Show code examples (Java · Python · .NET · PHP)
- Java
- Python
- .NET
- PHP
package com.kody;
import com.kodypay.grpc.ecom.v1.KodyEcomPaymentsServiceGrpc;
import com.kodypay.grpc.ecom.v1.PaymentInitiationRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;
import java.util.UUID;
public class InitiatePaymentStreamExample {
public static final String HOSTNAME = "HOSTNAME";
public static final String API_KEY = "API_KEY";
public static void main(String[] args) {
String storeId = "STORE_ID";
initiateEcomPaymentStream(storeId);
}
public static void initiateEcomPaymentStream(String storeId) {
// Create a managed channel and attach the API key metadata.
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);
var paymentClient = KodyEcomPaymentsServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
String orderId = "order_" + UUID.randomUUID();
String paymentReference = "pay_" + UUID.randomUUID();
PaymentInitiationRequest request = PaymentInitiationRequest.newBuilder()
.setStoreId(storeId)
.setPaymentReference(paymentReference)
.setAmountMinorUnits(1000) // e.g., 1000 for £10.00
.setCurrency("GBP")
.setOrderId(orderId)
.setReturnUrl("https://your-return-url.com")
// Optionally, set additional fields:
.setPayerEmailAddress("customer@example.com")
.build();
// Since InitiatePaymentStream returns a stream, get the first response from the iterator
var response = paymentClient.initiatePaymentStream(request).next();
if (response.hasResponse()) {
var details = response.getResponse();
System.out.println("Payment ID: " + details.getPaymentId());
System.out.println("Status: " + details.getStatus());
// The stream reuses PaymentDetailsResponse; the hosted payment page
// link is returned via payment_data.payment_wallet.payment_link_id.
if (details.hasPaymentData() && details.getPaymentData().hasPaymentWallet()) {
System.out.println("Payment page: " + details.getPaymentData().getPaymentWallet().getPaymentLinkId());
}
} else {
System.err.println("Error: " + response.getError().getMessage());
}
channel.shutdownNow();
}
}
import grpc
import kody_clientsdk_python.ecom.v1.ecom_pb2 as kody_model
import kody_clientsdk_python.ecom.v1.ecom_pb2_grpc as kody_client
from uuid import uuid4
def initiate_payment():
hostname = "HOSTNAME"
api_key = "API_KEY"
store_id = "STORE_ID"
with grpc.secure_channel(hostname, grpc.ssl_channel_credentials()) as channel:
client = kody_client.KodyEcomPaymentsServiceStub(channel)
request = kody_model.PaymentInitiationRequest(
store_id=store_id,
payment_reference=f"pay_{uuid4()}",
amount_minor_units=1000, # e.g., 1000 for £10.00
currency="GBP",
order_id=f"order_{uuid4()}",
return_url="https://your-return-url.com",
payer_email_address="customer@example.com" # Optional
)
response_iterator = client.InitiatePaymentStream(request, metadata=[("x-api-key", api_key)])
# Process the first response from the stream
for response in response_iterator:
if response.HasField("response"):
details = response.response
print(f"Payment ID: {details.payment_id}")
print(f"Status: {kody_model.PaymentStatus.Name(details.status)}")
# The stream reuses PaymentDetailsResponse; the hosted payment page
# link is returned via payment_data.payment_wallet.payment_link_id.
if details.HasField("payment_data") and details.payment_data.HasField("payment_wallet"):
print(f"Payment page: {details.payment_data.payment_wallet.payment_link_id}")
else:
print(f"Error: {response.error.message}")
if __name__ == "__main__":
initiate_payment()
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Ecom.V1;
class Program
{
static async Task Main()
{
string hostname = "https://HOSTNAME";
string apiKey = "API_KEY";
var channel = GrpcChannel.ForAddress(hostname);
var client = new KodyEcomPaymentsService.KodyEcomPaymentsServiceClient(channel);
var metadata = new Metadata { { "X-API-Key", apiKey } };
var request = new PaymentInitiationRequest
{
StoreId = "STORE_ID",
PaymentReference = $"pay_{Guid.NewGuid()}",
AmountMinorUnits = 1000, // e.g., 1000 for £10.00
Currency = "GBP",
OrderId = $"order_{Guid.NewGuid()}",
ReturnUrl = "https://your-return-url.com",
PayerEmailAddress = "customer@example.com" // Optional
};
using var call = client.InitiatePaymentStream(request, metadata);
while (await call.ResponseStream.MoveNext())
{
var response = call.ResponseStream.Current;
if (response.ResultCase == PaymentDetailsResponse.ResultOneofCase.Response)
{
var details = response.Response;
Console.WriteLine($"Payment ID: {details.PaymentId}");
Console.WriteLine($"Status: {details.Status}");
// The stream reuses PaymentDetailsResponse; the hosted payment page
// link is returned via payment_data.payment_wallet.payment_link_id.
if (details.PaymentData?.PaymentWallet != null)
{
Console.WriteLine($"Payment page: {details.PaymentData.PaymentWallet.PaymentLinkId}");
}
}
else
{
Console.WriteLine($"Error: {response.Error.Message}");
}
}
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Ecom\V1\KodyEcomPaymentsServiceClient;
use Com\Kodypay\Ecom\V1\PaymentInitiationRequest;
use Grpc\ChannelCredentials;
$client = new KodyEcomPaymentsServiceClient('HOSTNAME:443', [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => ['API_KEY']];
$request = new PaymentInitiationRequest();
$request->setStoreId('STORE_ID');
$request->setPaymentReference('pay_' . uniqid());
$request->setAmountMinorUnits(1000); // e.g., 1000 for £10.00
$request->setCurrency('GBP');
$request->setOrderId('order_' . uniqid());
$request->setReturnUrl('https://your-return-url.com');
// Optionally, set additional fields:
$request->setPayerEmailAddress("customer@example.com");
$call = $client->InitiatePaymentStream($request, $metadata);
foreach ($call->responses() as $response) {
if ($response->getResponse()) {
$details = $response->getResponse();
echo "Payment ID: " . $details->getPaymentId() . PHP_EOL;
echo "Status: " . $details->getStatus() . PHP_EOL;
// The stream reuses PaymentDetailsResponse; the hosted payment page
// link is returned via payment_data.payment_wallet.payment_link_id.
if ($details->getPaymentData() && $details->getPaymentData()->getPaymentWallet()) {
echo "Payment page: " . $details->getPaymentData()->getPaymentWallet()->getPaymentLinkId() . PHP_EOL;
}
} else {
echo "Error: " . $response->getError()->getMessage() . PHP_EOL;
}
}
3. Payment Details
Method: KodyEcomPaymentsService.PaymentDetails
Retrieves details of a specific payment using either the payment_id or the payment_reference.
Poll every 2–5 seconds to avoid excessive load while still getting timely status updates.
Request
rpc PaymentDetails(PaymentDetailsRequest) returns (PaymentDetailsResponse);
message PaymentDetailsRequest {
string store_id = 1; // Your Kody store id
oneof payment_identifier {
string payment_id = 2; // Kody-generated payment identifier
string payment_reference = 3; // Your unique payment reference from initiation
}
}
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | Your Kody store ID. |
payment_id | string | One of | Kody-generated payment identifier. |
payment_reference | string | One of | Your payment reference from initiation. |
Response
message PaymentDetailsResponse {
oneof result {
PaymentDetails response = 1;
Error error = 2;
}
message PaymentDetails {
string payment_id = 1; // Kody-generated payment identifier
PaymentStatus status = 5;
google.protobuf.Timestamp date_created = 7;
// Detailed payment data (if available)
optional PaymentData payment_data = 11;
// Sale-related data (if applicable)
optional SaleData sale_data = 12;
// Refund fields — available from protocol v1.8.7
repeated RefundDetails refunds = 13; // Every refund taken against this payment, oldest first
uint64 amount_refunded_minor_units = 14; // Refunded so far, counting only refunds that succeeded
bool fully_refunded = 15; // True only when the whole payment has been refunded
}
// One refund taken against the payment. Available from protocol v1.8.7.
message RefundDetails {
string payment_transaction_id = 1; // This refund's own id. Use it to identify this refund
optional string refund_psp_reference = 2; // Absent until the acquirer has accepted the refund
string refund_amount = 3; // BigDecimal/2.dp (e.g. "10.00")
google.protobuf.Timestamp event_date = 4;
Refund.Status status = 5; // What this refund is doing — see Common enums
}
message Error {
Type type = 1;
string message = 2;
enum Type {
UNKNOWN = 0;
NOT_FOUND = 1;
INVALID_REQUEST = 2;
}
}
}
// PaymentData contains detailed information about the payment method and authorisation
message PaymentData {
string psp_reference = 1; // Payment service provider reference
optional PaymentMethods payment_method = 2; // Payment method (VISA, MASTERCARD, etc.). Explicit presence: VISA is the zero value, so check presence before reading.
string payment_method_variant = 3; // Variant of the payment method
PaymentAuthStatus auth_status = 4; // Authorisation status
google.protobuf.Timestamp auth_status_date = 5; // Date/time of the auth status change
enum PaymentAuthStatus {
PENDING = 0;
AUTHORISED = 1;
FAILED = 2;
CAPTURED = 3;
RELEASED = 4;
EXPIRED = 5;
}
oneof payment_method_details {
PaymentCard payment_card = 6; // Card payment details
PaymentWallet payment_wallet = 7; // Digital wallet payment details
}
message PaymentCard {
string card_last_4_digits = 1; // Last 4 digits of the card
string auth_code = 2; // Authorisation code
string payment_token = 3; // Card token (if tokenisation was requested)
}
message PaymentWallet {
optional string card_last_4_digits = 1; // Last 4 digits (if available)
string payment_link_id = 2; // Wallet payment link identifier
}
}
// SaleData contains information about the sale/order
message SaleData {
uint64 amount_minor_units = 1; // Amount in minor units (e.g., 2000 for £20.00)
string currency = 2; // ISO 4217 currency code
string order_id = 3; // Your order identifier
string payment_reference = 4; // Your payment reference
optional string order_metadata = 5; // Additional order data
}
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_minor_units |
| Whether the payment is fully refunded | fully_refunded |
| What happened to an individual refund | refunds[].status |
amount_refunded_minor_units 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. Both aggregates are derived from the rows in refunds, so they cannot disagree
with the list.
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.
refunds, amount_refunded_minor_units, fully_refunded, the RefundDetails message and
Refund.Status were all added in protocol v1.8.7. See Protocol versions and SDK
releases for what
that means for your language.
PaymentDetails is on the PaymentDetails message, which several endpoints return — but only
Payment Details and Payment Details Stream
populate the refund fields. Get Payments and
Get Token Payment Details return the same message with the refund fields
left unset.
Do not read the refund fields from those endpoints — a fully refunded payment will look like one that was never refunded. Use Get Payments to find payments, then call Payment Details for each one whose refund state you need.
On protocol v1.8.7 the two aggregates have no presence, so an endpoint that does not report them
returns 0 and false and you cannot tell that apart from a payment with nothing refunded. They
are optional from the next protocol version, so a newer client can check presence — but refunds
is a repeated field and has no presence either way, so the rule above still stands.
Examples
Show code examples (Java · Python · .NET · PHP)
- Java
- Python
- .NET
- PHP
package com.kody;
import com.kodypay.grpc.ecom.v1.KodyEcomPaymentsServiceGrpc;
import com.kodypay.grpc.ecom.v1.PaymentDetailsRequest;
import com.kodypay.grpc.ecom.v1.PaymentDetailsResponse;
import com.kodypay.grpc.ecom.v1.PaymentDetailsResponse.PaymentDetails;
import com.kodypay.grpc.ecom.v1.PaymentDetailsResponse.PaymentData;
import com.kodypay.grpc.ecom.v1.PaymentDetailsResponse.SaleData;
import com.kodypay.grpc.ecom.v1.PaymentStatus;
import com.kodypay.grpc.ecom.v1.PaymentDetailsResponse.PaymentData.PaymentCard;
import com.kodypay.grpc.ecom.v1.PaymentDetailsResponse.PaymentData.PaymentWallet;
import com.kodypay.grpc.ecom.v1.PaymentDetailsResponse.PaymentData.PaymentMethodDetailsCase;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;
public class GetPaymentDetailsExample {
public static PaymentDetails getPaymentDetails(String storeId, String paymentId) throws InterruptedException {
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");
KodyEcomPaymentsServiceGrpc.KodyEcomPaymentsServiceBlockingStub client =
KodyEcomPaymentsServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
PaymentDetailsRequest request = PaymentDetailsRequest.newBuilder()
.setStoreId(storeId)
.setPaymentId(paymentId)
.build();
PaymentDetails details;
PaymentStatus status;
do {
PaymentDetailsResponse response = client.paymentDetails(request);
if (!response.hasResponse()) {
throw new RuntimeException("Error: " + response.getError().getMessage());
}
details = response.getResponse();
status = details.getStatus();
System.out.println("Current Status: " + status);
if (status == PaymentStatus.PENDING) {
Thread.sleep(2000);
}
} while (status == PaymentStatus.PENDING);
channel.shutdownNow();
return details;
}
public static void main(String[] args) throws InterruptedException {
String storeId = "STORE_ID";
String paymentId = "PAYMENT_ID";
PaymentDetails details = getPaymentDetails(storeId, paymentId);
printPaymentDetails(details);
}
public static void printPaymentDetails(PaymentDetails details) {
if (details.hasPaymentData()) {
PaymentData paymentData = details.getPaymentData();
System.out.println("PSP Reference: " + paymentData.getPspReference());
System.out.println("Payment Method: " + paymentData.getPaymentMethod());
System.out.println("Auth Status: " + paymentData.getAuthStatus());
PaymentMethodDetailsCase methodCase = paymentData.getPaymentMethodDetailsCase();
switch (methodCase) {
case PAYMENT_CARD -> {
PaymentCard card = paymentData.getPaymentCard();
System.out.println("Card Last 4: " + card.getCardLast4Digits());
System.out.println("Auth Code: " + card.getAuthCode());
System.out.println("Payment Token: " + card.getPaymentToken());
}
case PAYMENT_WALLET -> {
PaymentWallet wallet = paymentData.getPaymentWallet();
if (wallet.hasCardLast4Digits()) {
System.out.println("Wallet Card Last 4: " + wallet.getCardLast4Digits());
}
System.out.println("Payment Link ID: " + wallet.getPaymentLinkId());
}
default -> System.out.println("Unknown payment method details.");
}
}
if (details.hasSaleData()) {
SaleData sale = details.getSaleData();
System.out.println("Amount: " + sale.getAmountMinorUnits());
System.out.println("Currency: " + sale.getCurrency());
System.out.println("Order ID: " + sale.getOrderId());
System.out.println("Payment Reference: " + sale.getPaymentReference());
if (sale.hasOrderMetadata()) {
System.out.println("Order Metadata: " + sale.getOrderMetadata());
}
}
}
}
import grpc
import time
import kody_clientsdk_python.ecom.v1.ecom_pb2 as kody_model
import kody_clientsdk_python.ecom.v1.ecom_pb2_grpc as kody_client
def get_payment_details(store_id: str, payment_id: str) -> kody_model.PaymentDetailsResponse.PaymentDetails:
with grpc.secure_channel("HOSTNAME:443", grpc.ssl_channel_credentials()) as channel:
client = kody_client.KodyEcomPaymentsServiceStub(channel)
request = kody_model.PaymentDetailsRequest(
store_id=store_id,
payment_id=payment_id
)
while True:
response = client.PaymentDetails(request, metadata=[("x-api-key", "API_KEY")])
if not response.HasField("response"):
raise RuntimeError(f"Error: {response.error.message}")
details = response.response
print(f"Current Status: {kody_model.PaymentStatus.Name(details.status)}")
if details.status != kody_model.PaymentStatus.PENDING:
return details
time.sleep(2)
def print_payment_details(details: kody_model.PaymentDetailsResponse.PaymentDetails) -> None:
if details.HasField("payment_data"):
data = details.payment_data
print(f"PSP Reference: {data.psp_reference}")
print(f"Payment Method: {data.payment_method}")
print(f"Auth Status: {data.auth_status}")
if data.HasField("payment_card"):
card = data.payment_card
print(f"Card Last 4: {card.card_last_4_digits}")
print(f"Auth Code: {card.auth_code}")
print(f"Payment Token: {card.payment_token}")
elif data.HasField("payment_wallet"):
wallet = data.payment_wallet
if wallet.HasField("card_last_4_digits"):
print(f"Wallet Card Last 4: {wallet.card_last_4_digits}")
print(f"Payment Link ID: {wallet.payment_link_id}")
if details.HasField("sale_data"):
sale = details.sale_data
print(f"Amount: {sale.amount_minor_units}")
print(f"Currency: {sale.currency}")
print(f"Order ID: {sale.order_id}")
print(f"Payment Reference: {sale.payment_reference}")
if sale.HasField("order_metadata"):
print(f"Order Metadata: {sale.order_metadata}")
using System;
using System.Threading.Tasks;
using System.Threading;
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Ecom.V1;
public class GetPaymentDetailsExample
{
private readonly KodyEcomPaymentsService.KodyEcomPaymentsServiceClient _client;
private readonly Metadata _metadata;
public GetPaymentDetailsExample()
{
var channel = GrpcChannel.ForAddress("https://HOSTNAME");
_client = new KodyEcomPaymentsService.KodyEcomPaymentsServiceClient(channel);
_metadata = new Metadata { { "X-API-Key", "API_KEY" } };
}
public async Task<PaymentDetails> GetPaymentDetails(string storeId, string paymentId)
{
var request = new PaymentDetailsRequest
{
StoreId = storeId,
PaymentId = paymentId
};
while (true)
{
var response = await _client.PaymentDetailsAsync(request, _metadata);
if (response.ResultCase != PaymentDetailsResponse.ResultOneofCase.Response)
{
throw new Exception($"Error: {response.Error.Message}");
}
var details = response.Response;
Console.WriteLine($"Current Status: {details.Status}");
if (details.Status != PaymentStatus.Pending)
return details;
Thread.Sleep(2000);
}
}
public void PrintPaymentDetails(PaymentDetails details)
{
if (details.HasPaymentData)
{
var data = details.PaymentData;
Console.WriteLine($"PSP Reference: {data.PspReference}");
Console.WriteLine($"Payment Method: {data.PaymentMethod}");
Console.WriteLine($"Auth Status: {data.AuthStatus}");
switch (data.PaymentMethodDetailsCase)
{
case PaymentData.PaymentMethodDetailsOneofCase.PaymentCard:
var card = data.PaymentCard;
Console.WriteLine($"Card Last 4: {card.CardLast4Digits}");
Console.WriteLine($"Auth Code: {card.AuthCode}");
Console.WriteLine($"Payment Token: {card.PaymentToken}");
break;
case PaymentData.PaymentMethodDetailsOneofCase.PaymentWallet:
var wallet = data.PaymentWallet;
if (wallet.HasCardLast4Digits)
Console.WriteLine($"Wallet Card Last 4: {wallet.CardLast4Digits}");
Console.WriteLine($"Payment Link ID: {wallet.PaymentLinkId}");
break;
}
}
if (details.HasSaleData)
{
var sale = details.SaleData;
Console.WriteLine($"Amount: {sale.AmountMinorUnits}");
Console.WriteLine($"Currency: {sale.Currency}");
Console.WriteLine($"Order ID: {sale.OrderId}");
Console.WriteLine($"Payment Reference: {sale.PaymentReference}");
if (sale.HasOrderMetadata)
Console.WriteLine($"Order Metadata: {sale.OrderMetadata}");
}
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Ecom\V1\KodyEcomPaymentsServiceClient;
use Com\Kodypay\Ecom\V1\PaymentDetailsRequest;
use Com\Kodypay\Ecom\V1\PaymentStatus;
function get_payment_details($storeId, $paymentId)
{
$client = new KodyEcomPaymentsServiceClient('HOSTNAME:443', [
'credentials' => Grpc\ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => ['API_KEY']];
$request = new PaymentDetailsRequest();
$request->setStoreId($storeId);
$request->setPaymentId($paymentId);
while (true) {
list($response, $status) = $client->PaymentDetails($request, $metadata)->wait();
if ($status->code !== Grpc\STATUS_OK || !$response->getResponse()) {
throw new Exception("Error: " . $response->getError()->getMessage());
}
$details = $response->getResponse();
echo "Current Status: " . $details->getStatus() . PHP_EOL;
if ($details->getStatus() !== PaymentStatus::PENDING) {
return $details;
}
sleep(2);
}
}
function print_payment_details($details)
{
if ($details->hasPaymentData()) {
$data = $details->getPaymentData();
echo "PSP Reference: " . $data->getPspReference() . PHP_EOL;
echo "Payment Method: " . $data->getPaymentMethod() . PHP_EOL;
echo "Auth Status: " . $data->getAuthStatus() . PHP_EOL;
switch ($data->getPaymentMethodDetails()) {
case 'payment_card':
$card = $data->getPaymentCard();
echo "Card Last 4: " . $card->getCardLast4Digits() . PHP_EOL;
echo "Auth Code: " . $card->getAuthCode() . PHP_EOL;
echo "Payment Token: " . $card->getPaymentToken() . PHP_EOL;
break;
case 'payment_wallet':
$wallet = $data->getPaymentWallet();
if ($wallet->hasCardLast4Digits()) {
echo "Wallet Card Last 4: " . $wallet->getCardLast4Digits() . PHP_EOL;
}
echo "Payment Link ID: " . $wallet->getPaymentLinkId() . PHP_EOL;
break;
}
}
if ($details->hasSaleData()) {
$sale = $details->getSaleData();
echo "Amount: " . $sale->getAmountMinorUnits() . PHP_EOL;
echo "Currency: " . $sale->getCurrency() . PHP_EOL;
echo "Order ID: " . $sale->getOrderId() . PHP_EOL;
echo "Payment Reference: " . $sale->getPaymentReference() . PHP_EOL;
if ($sale->hasOrderMetadata()) {
echo "Order Metadata: " . $sale->getOrderMetadata() . PHP_EOL;
}
}
}
4. Payment Details Stream
Method: KodyEcomPaymentsService.PaymentDetailsStream
Retrieves details of a specific payment using either the payment_id or the payment_reference.
Streams updates until the transaction reaches a terminal state, then returns the final result.
Request
rpc PaymentDetailsStream(PaymentDetailsRequest) returns (stream PaymentDetailsResponse);
message PaymentDetailsRequest {
string store_id = 1; // Your Kody store id
oneof payment_identifier {
string payment_id = 2; // Kody-generated payment identifier
string payment_reference = 3; // Your unique payment reference from initiation
}
}
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | Your Kody store ID. |
payment_id | string | One of | Kody-generated payment identifier. |
payment_reference | string | One of | Your payment reference from initiation. |
Response
message PaymentDetailsResponse {
oneof result {
PaymentDetails response = 1;
Error error = 2;
}
message PaymentDetails {
string payment_id = 1; // Kody-generated payment identifier
PaymentStatus status = 5;
google.protobuf.Timestamp date_created = 7;
// Detailed payment data (if available)
optional PaymentData payment_data = 11;
// Sale-related data (if applicable)
optional SaleData sale_data = 12;
// Refund fields — available from protocol v1.8.7
repeated RefundDetails refunds = 13; // Every refund taken against this payment, oldest first
uint64 amount_refunded_minor_units = 14; // Refunded so far, counting only refunds that succeeded
bool fully_refunded = 15; // True only when the whole payment has been refunded
}
// One refund taken against the payment. Available from protocol v1.8.7.
message RefundDetails {
string payment_transaction_id = 1; // This refund's own id. Use it to identify this refund
optional string refund_psp_reference = 2; // Absent until the acquirer has accepted the refund
string refund_amount = 3; // BigDecimal/2.dp (e.g. "10.00")
google.protobuf.Timestamp event_date = 4;
Refund.Status status = 5; // What this refund is doing — see Common enums
}
// PaymentData contains detailed information about the payment method and authorisation
message PaymentData {
string psp_reference = 1; // Payment service provider reference
optional PaymentMethods payment_method = 2; // Payment method (VISA, MASTERCARD, etc.). Explicit presence: VISA is the zero value, so check presence before reading.
string payment_method_variant = 3; // Variant of the payment method
PaymentAuthStatus auth_status = 4; // Authorisation status
google.protobuf.Timestamp auth_status_date = 5; // Date/time of the auth status change
enum PaymentAuthStatus {
PENDING = 0;
AUTHORISED = 1;
FAILED = 2;
CAPTURED = 3;
RELEASED = 4;
EXPIRED = 5;
}
oneof payment_method_details {
PaymentCard payment_card = 6; // Card payment details
PaymentWallet payment_wallet = 7; // Digital wallet payment details
}
message PaymentCard {
string card_last_4_digits = 1; // Last 4 digits of the card
string auth_code = 2; // Authorisation code
string payment_token = 3; // Card token (if tokenisation was requested)
}
message PaymentWallet {
optional string card_last_4_digits = 1; // Last 4 digits (if available)
string payment_link_id = 2; // Wallet payment link identifier
}
}
// SaleData contains information about the sale/order
message SaleData {
uint64 amount_minor_units = 1; // Amount in minor units (e.g., 2000 for £20.00)
string currency = 2; // ISO 4217 currency code
string order_id = 3; // Your order identifier
string payment_reference = 4; // Your payment reference
optional string order_metadata = 5; // Additional order data
}
message Error {
Type type = 1;
string message = 2;
enum Type {
UNKNOWN = 0;
NOT_FOUND = 1;
INVALID_REQUEST = 2;
}
}
}
Examples
Show code examples (Java · Python · .NET · PHP)
- Java
- Python
- .NET
- PHP
package com.kody;
import com.kodypay.grpc.ecom.v1.KodyEcomPaymentsServiceGrpc;
import com.kodypay.grpc.ecom.v1.PaymentDetailsRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;
public class GetPaymentDetailsExample {
public static void getPaymentDetails(String storeId, String paymentId) {
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");
var client = KodyEcomPaymentsServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
PaymentDetailsRequest request = PaymentDetailsRequest.newBuilder()
.setStoreId(storeId)
.setPaymentId(paymentId)
.build();
//Since PaymentDetailsStream returns a stream, get the first response from the iterator
var response = client.paymentDetailsStream(request).next();
if (response.hasResponse()) {
System.out.println("Status: " + response.getResponse().getStatus());
System.out.println("Payment ID: " + response.getResponse().getPaymentId());
// Additional fields (e.g. payment_data) can be inspected as needed
} else {
System.err.println("Error: " + response.getError().getMessage());
}
channel.shutdownNow();
}
public static void main(String[] args) {
getPaymentDetails("STORE_ID", "PAYMENT_ID");
}
}
import grpc
import kody_clientsdk_python.ecom.v1.ecom_pb2 as kody_model
import kody_clientsdk_python.ecom.v1.ecom_pb2_grpc as kody_client
def get_payment_details(store_id: str, payment_id: str) -> None:
with grpc.secure_channel("HOSTNAME:443", grpc.ssl_channel_credentials()) as channel:
client = kody_client.KodyEcomPaymentsServiceStub(channel)
request = kody_model.PaymentDetailsRequest(
store_id=store_id,
payment_id=payment_id
)
response_iterator = client.PaymentDetailsStream(request, metadata=[("x-api-key", "API_KEY")])
# Process the first response from the stream
for response in response_iterator:
if response.HasField("response"):
print(f"Payment ID: {response.response.payment_id}")
print(f"Status: {response.response.status}")
# Additional fields (e.g. payment_data) can be inspected as needed
else:
print(f"Error: {response.error.message}")
if __name__ == "__main__":
get_payment_details("STORE_ID", "PAYMENT_ID")
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Ecom.V1;
public class GetPaymentDetailsExample
{
public async Task GetPaymentDetails(string storeId, string paymentId)
{
var channel = GrpcChannel.ForAddress("https://HOSTNAME");
var client = new KodyEcomPaymentsService.KodyEcomPaymentsServiceClient(channel);
var metadata = new Metadata { { "X-API-Key", "API_KEY" } };
var request = new PaymentDetailsRequest
{
StoreId = storeId,
PaymentId = paymentId
};
using var call = client.PaymentDetailsStream(request, metadata);
while (await call.ResponseStream.MoveNext())
{
var response = call.ResponseStream.Current;
if (response.ResultCase == PaymentDetailsResponse.ResultOneofCase.Response)
{
Console.WriteLine($"Payment ID: {response.Response.PaymentId}");
Console.WriteLine($"Status: {response.Response.Status}");
// Additional fields (e.g. payment_data) can be inspected as needed
}
else
{
Console.WriteLine($"Error: {response.Error.Message}");
}
}
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Ecom\V1\KodyEcomPaymentsServiceClient;
use Com\Kodypay\Ecom\V1\PaymentDetailsRequest;
use Grpc\ChannelCredentials;
$client = new KodyEcomPaymentsServiceClient('HOSTNAME:443', [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => ['API_KEY']];
$request = new PaymentDetailsRequest();
$request->setStoreId('STORE_ID');
$request->setPaymentId('PAYMENT_ID');
$call = $client->PaymentDetailsStream($request, $metadata);
foreach ($call->responses() as $response) {
if ($response->getResponse()) {
echo "Payment ID: " . $response->getResponse()->getPaymentId().PHP_EOL;
echo "Payment Link ID: " . $response->getResponse()->getPaymentData()->getPaymentWallet()->getPaymentLinkId().PHP_EOL;
// Additional fields (e.g. payment_data) can be inspected as needed
} else {
echo "Error: " . $response->getError()->getMessage().PHP_EOL;
break;
}
}
5. Get Payments
The PaymentDetails entries this endpoint returns carry the refund fields declared on the message,
but this endpoint does not fill them in: refunds comes back empty, and the two aggregates come back
unset whatever the payment's real refund state is. Call Payment Details for a
payment whose refunds you need.
Method: KodyEcomPaymentsService.GetPayments
Retrieves a paginated list of payments with optional filters.
Request
message GetPaymentsRequest {
string store_id = 1;
PageCursor page_cursor = 2;
Filter filter = 3;
message PageCursor {
int64 page = 1;
int64 page_size = 2;
}
message Filter {
optional string order_id = 1;
optional google.protobuf.Timestamp created_before = 2;
}
}
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | Your Kody store ID. |
page_cursor | PageCursor | Yes | Pagination cursor: page and page_size. |
filter | Filter | No | Optional filters: order_id, created_before. |
Response
message GetPaymentsResponse {
oneof result {
Response response = 1;
Error error = 2;
}
message Response {
int64 total = 2;
repeated PaymentDetailsResponse.PaymentDetails payments = 3;
}
message Error {
Type type = 1;
string message = 2;
enum Type {
UNKNOWN = 0;
NOT_FOUND = 1;
INVALID_ARGUMENT = 2;
}
}
}
Examples
Show code examples (Java · Python · .NET · PHP)
- Java
- Python
- .NET
- PHP
package com.kody;
import com.kodypay.grpc.ecom.v1.KodyEcomPaymentsServiceGrpc;
import com.kodypay.grpc.ecom.v1.GetPaymentsRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;
public class GetPaymentsExample {
public static void getPayments(String storeId) {
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");
var client = KodyEcomPaymentsServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
var request = GetPaymentsRequest.newBuilder()
.setStoreId(storeId)
.setPageCursor(GetPaymentsRequest.PageCursor.newBuilder().setPage(0).setPageSize(10))
.build();
var response = client.getPayments(request);
if (response.hasResponse()) {
System.out.println("Total payments: " + response.getResponse().getTotal());
for (var payment : response.getResponse().getPaymentsList()) {
System.out.println("Payment ID: " + payment.getPaymentId());
System.out.println("Status: " + payment.getStatus());
System.out.println("Created: " + payment.getDateCreated());
}
} else {
System.err.println("Error: " + response.getError().getMessage());
}
channel.shutdownNow();
}
public static void main(String[] args) {
getPayments("STORE_ID");
}
}
import grpc
import kody_clientsdk_python.ecom.v1.ecom_pb2 as kody_model
import kody_clientsdk_python.ecom.v1.ecom_pb2_grpc as kody_client
def get_payments(store_id: str) -> None:
with grpc.secure_channel("HOSTNAME:443", grpc.ssl_channel_credentials()) as channel:
client = kody_client.KodyEcomPaymentsServiceStub(channel)
request = kody_model.GetPaymentsRequest(
store_id=store_id,
page_cursor=kody_model.GetPaymentsRequest.PageCursor(page=0, page_size=10)
)
response = client.GetPayments(request, metadata=[("x-api-key", "API_KEY")])
if response.HasField("response"):
print(f"Total payments: {response.response.total}")
for payment in response.response.payments:
print(f"Payment ID: {payment.payment_id}")
print(f"Status: {payment.status}")
else:
print(f"Error: {response.error.message}")
if __name__ == "__main__":
get_payments("STORE_ID")
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Ecom.V1;
public class GetPaymentsExample
{
public async Task GetPayments(string storeId)
{
var channel = GrpcChannel.ForAddress("https://HOSTNAME");
var client = new KodyEcomPaymentsService.KodyEcomPaymentsServiceClient(channel);
var metadata = new Metadata { { "X-API-Key", "API_KEY" } };
var request = new GetPaymentsRequest
{
StoreId = storeId,
PageCursor = new PageCursor { Page = 1, PageSize = 10 }
};
var response = await client.GetPaymentsAsync(request, metadata);
if (response.ResultCase == GetPaymentsResponse.ResultOneofCase.Response)
{
Console.WriteLine($"Total payments: {response.Response.Total}");
foreach (var payment in response.Response.Payments)
{
Console.WriteLine($"Payment ID: {payment.PaymentId}");
Console.WriteLine($"Status: {payment.Status}");
}
}
else
{
Console.WriteLine($"Error: {response.Error.Message}");
}
}
}
<?php
require __DIR__ . '/../vendor/autoload.php';
use Com\Kodypay\Ecom\V1\KodyEcomPaymentsServiceClient;
use Com\Kodypay\Ecom\V1\GetPaymentsRequest;
use Grpc\ChannelCredentials;
$client = new KodyEcomPaymentsServiceClient('HOSTNAME:443', [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => ['API_KEY']];
$request = new GetPaymentsRequest();
$request->setStoreId('STORE_ID');
$pageCursor = new GetPaymentsRequest\PageCursor();
$pageCursor->setPage(0);
$pageCursor->setPageSize(10);
$request->setPageCursor($pageCursor);
list($response, $status) = $client->GetPayments($request, $metadata)->wait();
if ($status->code === \Grpc\STATUS_OK && $response->getResponse()) {
echo "Total payments: " . $response->getResponse()->getTotal() . PHP_EOL;
foreach ($response->getResponse()->getPayments() as $payment) {
echo "Payment ID: " . $payment->getPaymentId() . PHP_EOL;
echo "Status: " . $payment->getStatus() . PHP_EOL;
}
} else {
echo "Error: " . $response->getError()->getMessage() . PHP_EOL;
}
6. Refund Payment
Method: KodyEcomPaymentsService.Refund
Issues a refund for a specific payment, either in full or in part. Identify the payment with its payment_id, and provide amount as a decimal string with two decimal places (e.g. "10.00").
psp_reference removedAs of 2026-09-05, refunding by psp_reference is no longer supported. A request that still sets it now carries no identifier and is rejected with INVALID_ARGUMENT rather than refunding anything. Identify the payment with payment_id.
Request
rpc Refund(RefundRequest) returns (stream RefundResponse);
message RefundRequest {
string store_id = 1; // UUID of store
// Refunding 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 with
// INVALID_ARGUMENT rather than refunding anything. Field 4 and the name stay reserved so neither
// can be reused for something else.
reserved 4;
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 id {
string payment_id = 2; // Payment id created by wombat
}
string amount = 3; // amount in BigDecimal/2.dp (0.00)
optional string idempotency_uuid = 5; // Idempotency key to ensure the request is processed only once, generated by client. Without it a retry is indistinguishable from a second partial refund and is processed as one.
}
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
store_id | string | Yes | Your store UUID. |
payment_id | string | Yes | Kody-generated payment ID. |
amount | string | Yes | Refund amount as a 2dp decimal string (e.g. "10.00"). |
idempotency_uuid | string | No | Client-generated idempotency key. Without it, a retry is indistinguishable from a second partial refund and is processed as a second refund. |
Response
message RefundResponse {
RefundStatus status = 1;
optional string failure_reason = 2; // Populated on failure
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; // This refund's own id — not the payment's
enum RefundStatus {
PENDING = 0;
REQUESTED = 1; // The refund request was accepted. Not yet money back
FAILED = 2;
}
}
Response fields
| Field | Type | Description |
|---|---|---|
status | RefundStatus | Whether the refund request was accepted — see the warning below. |
payment_id | string | The payment that was refunded. |
paymentTransactionId | string | This refund's own id. Use it to find this refund in PaymentDetails.refunds. |
total_paid_amount | string | The payment's total. |
total_amount_refunded | string | Refunded so far against this payment. |
remaining_amount | string | Still refundable. |
total_amount_requested | string | Requested against this payment. |
failure_reason | string | Populated on failure. |
paymentTransactionId — meaning changed in protocol v1.8.6Before v1.8.6 this field carried the payment's transaction id, which left the refund you had just created impossible to identify. From v1.8.6 it carries the refund's own id. If you stored this value as a payment identifier, revisit that assumption when you upgrade.
idempotency_uuid — available from protocol v1.8.6Send one. Without it a network retry is indistinguishable from a second, deliberate partial refund and is processed as one. With it, a replay is recognised instead of taking a second refund.
status reports acceptance, not outcomeREQUESTED means Kody accepted the refund request. It does not mean the money has been
returned — the acquirer offers no synchronous refund, so the outcome arrives afterwards. To learn
what actually happened, read refunds[].status on Payment Details, matching on
the paymentTransactionId returned here.
Examples
Show code examples (Java · Python · .NET · PHP)
- Java
- Python
- .NET
- PHP
package com.kody;
import com.kodypay.grpc.ecom.v1.KodyEcomPaymentsServiceGrpc;
import com.kodypay.grpc.ecom.v1.RefundRequest;
import com.kodypay.grpc.ecom.v1.RefundResponse;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;
public class RefundPaymentExample {
public static void main(String[] args) {
refundPayment("STORE_ID", "PAYMENT_ID");
}
public static void refundPayment(String storeId, String paymentId) {
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");
var client = KodyEcomPaymentsServiceGrpc.newBlockingStub(channel)
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
RefundRequest request = RefundRequest.newBuilder()
.setStoreId(storeId)
// Use payment_id (or alternatively setPspReference if available)
.setPaymentId(paymentId)
.setAmount("5.00") // Refund amount as a string (e.g., "5.00")
.build();
var responseIterator = client.refund(request);
while (responseIterator.hasNext()) {
RefundResponse response = responseIterator.next();
System.out.println("Refund Status: " + response.getStatus());
if (response.getStatus() == RefundResponse.RefundStatus.FAILED) {
System.err.println("Failure Reason: " + response.getFailureReason());
}
System.out.println("Total Amount Refunded: " + response.getTotalAmountRefunded());
}
channel.shutdownNow();
}
}
import grpc
from kody_clientsdk_python.ecom.v1 import ecom_pb2 as kody_model
from kody_clientsdk_python.ecom.v1 import ecom_pb2_grpc as kody_client
def refund_payment(store_id: str, payment_id: str) -> None:
with grpc.secure_channel("HOSTNAME:443", grpc.ssl_channel_credentials()) as channel:
client = kody_client.KodyEcomPaymentsServiceStub(channel)
request = kody_model.RefundRequest(
store_id=store_id,
payment_id=payment_id,
amount="5.00" # Refund amount as a string, e.g., "5.00"
)
responses = client.Refund(request, metadata=[("x-api-key", "API_KEY")])
for response in responses:
print(f"Refund Status: {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__":
refund_payment("STORE_ID", "PAYMENT_ID")
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Net.Client;
using Com.Kodypay.Ecom.V1;
public class RefundPaymentExample
{
public async Task RefundPayment(string storeId, string paymentId)
{
var channel = GrpcChannel.ForAddress("https://HOSTNAME");
var client = new KodyEcomPaymentsService.KodyEcomPaymentsServiceClient(channel);
var metadata = new Metadata { { "X-API-Key", "API_KEY" } };
var request = new RefundRequest
{
StoreId = storeId,
PaymentId = paymentId, // or set PspReference if needed
Amount = "5.00" // Refund amount as a string
};
using var call = client.Refund(request, metadata);
while (await call.ResponseStream.MoveNext())
{
var response = call.ResponseStream.Current;
Console.WriteLine($"Refund 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\Ecom\V1\KodyEcomPaymentsServiceClient;
use Com\Kodypay\Ecom\V1\RefundRequest;
use Grpc\ChannelCredentials;
$client = new KodyEcomPaymentsServiceClient('HOSTNAME:443', [
'credentials' => ChannelCredentials::createSsl()
]);
$metadata = ['X-API-Key' => ['API_KEY']];
$request = new RefundRequest();
$request->setStoreId('STORE_ID');
// Use payment_id (or alternatively, setPspReference if available)
$request->setPaymentId('PAYMENT_ID');
$request->setAmount("5.00"); // Refund amount as a string
$call = $client->Refund($request, $metadata);
foreach ($call->responses() as $response) {
echo "Refund Status: " . $response->getStatus() . PHP_EOL;
if ($response->getStatus() === \Com\Kodypay\Ecom\V1\RefundResponse\RefundStatus::FAILED) {
echo "Failure Reason: " . $response->getFailureReason() . PHP_EOL;
}
echo "Total Amount Refunded: " . $response->getTotalAmountRefunded() . PHP_EOL;
}
Need help?
For further support or more detailed information, contact the Kody Support team at support@kody.com.