Refund POS Charge
Refunds a previously approved POS charge. Supports both full and partial refunds by specifying the refund amount. The charge must be in an approved state to be eligible for a refund.
MSK encryption: Request and response bodies use MSK-encrypted envelopes (TR31-DATA). HMAC is computed over the encrypted request body. See End-to-End Example.
Purpose
- Issue a full or partial refund for a completed charge
- Track refund reason for reconciliation and reporting
- Return a unique refund ID for tracking the refund status
Authentication
POS HMAC Signature — requires device identification headers and HMAC signature for every request.
Device Identification Headers
| Header | Required | Format | Description |
|---|---|---|---|
X-Terminal-Id | Yes | Terminal ID string | Identifier of the terminal processing the refund. Example: TERM-001 |
User-Agent | Yes | Viopay-POS/{version} ({OS}; {model}) | POS app version string. Must start with Viopay-POS/. Example: Viopay-POS/1.4.2 (Android 12; PAX-A920) |
X-Client-Type | Yes | pos | Client type identifier. Always pos for POS devices. |
Signature Headers
| Header | Required | Format | Description |
|---|---|---|---|
X-Key-Id | Yes | Free-form string | Identifier of the HMAC key used to generate the signature. Example: hmac-key-2025-01 |
X-Timestamp | Yes | RFC3339 | Current time in UTC. Must be within 5 minutes of server time. Example: 2025-12-13T14:25:30Z |
X-Nonce | Yes | Alphanumeric string | Unique random value for each request. At least 12 characters. Example: a1b2c3d4e5f6 |
X-Signature | Yes | v1={base64_hmac} | HMAC-SHA256 signature over the request. See Signature Computation. |
See the Headers Reference for complete details on all POS headers.
Request
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
chargeID | string | Yes | The charge ID to refund (returned from Create POS Charge) |
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
amount | object | Yes | Refund amount |
amount.value | string | Yes | Amount in minor units (e.g. "1500" for 15.00). For a full refund, use the original charge amount. For a partial refund, use a smaller value. |
amount.currency | string | Yes | ISO 4217 currency code. Must match the original charge currency. |
reason | string | No | Reason for the refund (e.g. "customer_request", "defective_product") |
transaction_reference | string | No | External reference ID for the refund transaction |
Body — MSK envelope (wire format)
{
"format": "TR31-DATA",
"ciphertext": "A1B2C3...",
"initialization_vector": "000102030405060708090A0B0C0D0E0F",
"mode": "CBC"
}
Plaintext (before MSK encrypt)
{
"amount": {
"value": "1500",
"currency": "EUR"
},
"reason": "customer_request",
"transaction_reference": "REF-2025-001"
}
Response
200 OK — MSK-encrypted envelope
Decrypt with MSK to obtain:
{
"refund_id": "rf_xyz789abc012",
"status": "approved",
"amount": {
"value": "1500",
"currency": "EUR"
}
}
| Field | Type | Description |
|---|---|---|
refund_id | string | Unique identifier for the refund |
status | string | Refund status: approved, declined, pending |
amount | object | Confirmed refund amount |
amount.value | string | Amount in minor units |
amount.currency | string | ISO 4217 currency code |
400 Bad Request
Returned when the charge ID is missing, the request body is invalid, or the refund amount exceeds the original charge.
{
"error": {
"code": "4000",
"message": "Invalid refund request",
"trace_id": "abc123..."
}
}
401 Unauthorized
Returned when HMAC signature validation fails.
{
"error": {
"code": "4001",
"message": "Invalid signature",
"trace_id": "abc123..."
}
}
500 Internal Server Error
Returned when the payment gateway returns an error or processing fails.
{
"error": {
"code": "5000",
"message": "Gateway processing error",
"trace_id": "abc123..."
}
}
Code Examples
- cURL
- Python
- Ruby
- Go
- C#
- PHP
- Java
curl -X POST https://api.viopay.io/pos/charge/ch_abc123def456/refund \
-H "X-Terminal-Id: TERM-001" \
-H "User-Agent: Viopay-POS/1.4.2 (Android 12; PAX-A920)" \
-H "X-Client-Type: pos" \
-H "X-Key-Id: hmac-key-2025-01" \
-H "X-Timestamp: 2025-12-13T14:25:30Z" \
-H "X-Nonce: a1b2c3d4e5f6" \
-H "X-Signature: v1=SGVsbG8gV29ybGQ=" \
-H "Content-Type: application/json" \
-d '{
"amount": {
"value": "1500",
"currency": "EUR"
},
"reason": "customer_request"
}'
import requests
import hashlib
import hmac
import base64
import json
import uuid
from datetime import datetime, timezone
terminal_id = "TERM-001"
key_id = "hmac-key-2025-01"
hmac_secret = b"your-hmac-secret"
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
nonce = uuid.uuid4().hex[:12]
charge_id = "ch_abc123def456"
body = {
"amount": {"value": "1500", "currency": "EUR"},
"reason": "customer_request",
}
body_json = json.dumps(body, separators=(",", ":"))
path = f"/pos/charge/{charge_id}/refund"
signature_string = f"POST\n{path}\n\n{body_json}\n{terminal_id}\n{timestamp}\n{nonce}"
signature = hmac.new(hmac_secret, signature_string.encode(), hashlib.sha256).digest()
signature_b64 = base64.b64encode(signature).decode()
headers = {
"X-Terminal-Id": terminal_id,
"User-Agent": "Viopay-POS/1.4.2 (Android 12; PAX-A920)",
"X-Client-Type": "pos",
"X-Key-Id": key_id,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": f"v1={signature_b64}",
"Content-Type": "application/json",
}
response = requests.post(
f"https://api.viopay.io/pos/charge/{charge_id}/refund",
headers=headers,
data=body_json,
)
data = response.json()
print(f"Refund ID: {data['refund_id']}")
print(f"Status: {data['status']}")
print(f"Amount: {data['amount']['value']} {data['amount']['currency']}")
require 'net/http'
require 'json'
require 'uri'
require 'openssl'
require 'base64'
require 'securerandom'
require 'time'
terminal_id = "TERM-001"
key_id = "hmac-key-2025-01"
hmac_secret = "your-hmac-secret"
timestamp = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
nonce = SecureRandom.hex(6)
charge_id = "ch_abc123def456"
body = {
amount: { value: "1500", currency: "EUR" },
reason: "customer_request"
}
body_json = JSON.generate(body)
path = "/pos/charge/#{charge_id}/refund"
signature_string = "POST\n#{path}\n\n#{body_json}\n#{terminal_id}\n#{timestamp}\n#{nonce}"
signature = Base64.strict_encode64(
OpenSSL::HMAC.digest("SHA256", hmac_secret, signature_string)
)
uri = URI("https://api.viopay.io/pos/charge/#{charge_id}/refund")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request["X-Terminal-Id"] = terminal_id
request["User-Agent"] = "Viopay-POS/1.4.2 (Android 12; PAX-A920)"
request["X-Client-Type"] = "pos"
request["X-Key-Id"] = key_id
request["X-Timestamp"] = timestamp
request["X-Nonce"] = nonce
request["X-Signature"] = "v1=#{signature}"
request["Content-Type"] = "application/json"
request.body = body_json
response = http.request(request)
data = JSON.parse(response.body)
puts "Refund ID: #{data['refund_id']}"
puts "Status: #{data['status']}"
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/google/uuid"
)
type RefundRequest struct {
Amount Amount `json:"amount"`
Reason string `json:"reason,omitempty"`
}
type Amount struct {
Value string `json:"value"`
Currency string `json:"currency"`
}
type RefundResponse struct {
RefundID string `json:"refund_id"`
Status string `json:"status"`
Amount Amount `json:"amount"`
}
func main() {
terminalID := "TERM-001"
keyID := "hmac-key-2025-01"
hmacSecret := []byte("your-hmac-secret")
timestamp := time.Now().UTC().Format(time.RFC3339)
nonce := uuid.New().String()[:12]
chargeID := "ch_abc123def456"
refund := RefundRequest{
Amount: Amount{Value: "1500", Currency: "EUR"},
Reason: "customer_request",
}
bodyBytes, _ := json.Marshal(refund)
path := fmt.Sprintf("/pos/charge/%s/refund", chargeID)
sigString := fmt.Sprintf("POST\n%s\n\n%s\n%s\n%s\n%s", path, string(bodyBytes), terminalID, timestamp, nonce)
mac := hmac.New(sha256.New, hmacSecret)
mac.Write([]byte(sigString))
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
url := fmt.Sprintf("https://api.viopay.io/pos/charge/%s/refund", chargeID)
req, _ := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
req.Header.Set("X-Terminal-Id", terminalID)
req.Header.Set("User-Agent", "Viopay-POS/1.4.2 (Android 12; PAX-A920)")
req.Header.Set("X-Client-Type", "pos")
req.Header.Set("X-Key-Id", keyID)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", fmt.Sprintf("v1=%s", signature))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result RefundResponse
json.Unmarshal(body, &result)
fmt.Printf("Refund ID: %s\n", result.RefundID)
fmt.Printf("Status: %s\n", result.Status)
fmt.Printf("Amount: %s %s\n", result.Amount.Value, result.Amount.Currency)
}
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var terminalId = "TERM-001";
var keyId = "hmac-key-2025-01";
var hmacSecret = Encoding.UTF8.GetBytes("your-hmac-secret");
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
var nonce = Guid.NewGuid().ToString("N")[..12];
var chargeId = "ch_abc123def456";
var body = new
{
amount = new { value = "1500", currency = "EUR" },
reason = "customer_request"
};
var bodyJson = JsonSerializer.Serialize(body);
var path = $"/pos/charge/{chargeId}/refund";
var sigString = $"POST\n{path}\n\n{bodyJson}\n{terminalId}\n{timestamp}\n{nonce}";
var signature = Convert.ToBase64String(
new HMACSHA256(hmacSecret).ComputeHash(Encoding.UTF8.GetBytes(sigString))
);
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, $"https://api.viopay.io/pos/charge/{chargeId}/refund");
request.Content = new StringContent(bodyJson, Encoding.UTF8, "application/json");
request.Headers.Add("X-Terminal-Id", terminalId);
request.Headers.Add("User-Agent", "Viopay-POS/1.4.2 (Android 12; PAX-A920)");
request.Headers.Add("X-Client-Type", "pos");
request.Headers.Add("X-Key-Id", keyId);
request.Headers.Add("X-Timestamp", timestamp);
request.Headers.Add("X-Nonce", nonce);
request.Headers.Add("X-Signature", $"v1={signature}");
var response = await client.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<JsonElement>(responseBody);
Console.WriteLine($"Refund ID: {data.GetProperty("refund_id")}");
Console.WriteLine($"Status: {data.GetProperty("status")}");
<?php
$terminalId = "TERM-001";
$keyId = "hmac-key-2025-01";
$hmacSecret = "your-hmac-secret";
$timestamp = gmdate("Y-m-d\TH:i:s\Z");
$nonce = bin2hex(random_bytes(6));
$chargeId = "ch_abc123def456";
$body = json_encode([
"amount" => ["value" => "1500", "currency" => "EUR"],
"reason" => "customer_request",
]);
$path = "/pos/charge/{$chargeId}/refund";
$sigString = "POST\n{$path}\n\n{$body}\n{$terminalId}\n{$timestamp}\n{$nonce}";
$signature = base64_encode(hash_hmac("sha256", $sigString, $hmacSecret, true));
$ch = curl_init("https://api.viopay.io/pos/charge/{$chargeId}/refund");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
"X-Terminal-Id: {$terminalId}",
"User-Agent: Viopay-POS/1.4.2 (Android 12; PAX-A920)",
"X-Client-Type: pos",
"X-Key-Id: {$keyId}",
"X-Timestamp: {$timestamp}",
"X-Nonce: {$nonce}",
"X-Signature: v1={$signature}",
"Content-Type: application/json",
],
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo "Refund ID: " . $data["refund_id"] . "\n";
echo "Status: " . $data["status"] . "\n";
import java.net.http.*;
import java.net.URI;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.UUID;
import java.time.Instant;
String terminalId = "TERM-001";
String keyId = "hmac-key-2025-01";
byte[] hmacSecret = "your-hmac-secret".getBytes();
String timestamp = Instant.now().toString();
String nonce = UUID.randomUUID().toString().replace("-", "").substring(0, 12);
String chargeId = "ch_abc123def456";
String body = """
{
"amount": {"value": "1500", "currency": "EUR"},
"reason": "customer_request"
}""";
String path = "/pos/charge/" + chargeId + "/refund";
String sigString = "POST\n" + path + "\n\n" + body + "\n" + terminalId + "\n" + timestamp + "\n" + nonce;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(hmacSecret, "HmacSHA256"));
String signature = Base64.getEncoder().encodeToString(mac.doFinal(sigString.getBytes()));
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.viopay.io/pos/charge/" + chargeId + "/refund"))
.POST(HttpRequest.BodyPublishers.ofString(body))
.header("X-Terminal-Id", terminalId)
.header("User-Agent", "Viopay-POS/1.4.2 (Android 12; PAX-A920)")
.header("X-Client-Type", "pos")
.header("X-Key-Id", keyId)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Signature", "v1=" + signature)
.header("Content-Type", "application/json")
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
What's Next?
- Need to void a charge before settlement? → Cancel POS Charge
- Create a new charge → Create POS Charge