Create POS Charge
Creates a new payment charge from the POS terminal. This is the primary endpoint for processing card payments through a physical device. It supports chip (EMV), swipe (magnetic stripe), and manual card entry methods. The endpoint also supports idempotency to safely retry failed requests without creating duplicate charges.
MSK encryption: Request and response bodies use MSK-encrypted envelopes (TR31-DATA, AES-256-CBC). Encrypt the charge JSON with MSK before sending; decrypt the response envelope with MSK. HMAC is computed over the raw encrypted JSON body (the envelope), not the plaintext charge object.
See End-to-End Example for a full charge walkthrough.
Purpose
- Process a card payment through the POS terminal
- Support multiple card entry modes (chip, swipe, contactless, manual)
- Return charge status and gateway data for transaction reconciliation
- Support idempotent requests via
Idempotency-Keyheader
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 charge. 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 below. |
Optional Headers
| Header | Required | Description |
|---|---|---|
Idempotency-Key | No | Unique key to make the request idempotent. If a charge with the same key already exists, the original response is returned instead of creating a new charge. |
Signature Computation
The HMAC-SHA256 signature is computed over a string composed of the following values joined by newlines (\n):
POST\n/pos/charge\n\n{request_body}\n{terminal_id}\n{timestamp}\n{nonce}
| Component | Description |
|---|---|
method | HTTP method — always POST |
path | Request path — /pos/charge |
query | Query string (empty for this endpoint) |
body | The raw JSON request body (MSK envelope, not plaintext) |
terminal_id | Value of X-Terminal-Id header |
timestamp | Value of X-Timestamp header |
nonce | Value of X-Nonce header |
See the Headers Reference for complete details on all POS headers.
Request
Wire format (MSK envelope)
Send the encrypted envelope as the HTTP body (this is what you sign for HMAC):
{
"format": "TR31-DATA",
"ciphertext": "A1B2C3...",
"initialization_vector": "000102030405060708090A0B0C0D0E0F",
"mode": "CBC"
}
Plaintext (before MSK encrypt)
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
card | object | Yes | Card details |
card.number | string | Yes | Card PAN (Primary Account Number) |
card.expiry_year | integer | Yes | Card expiry year (4 digits) |
card.expiry_month | integer | Yes | Card expiry month (1–12) |
amount | object | Yes | Charge amount |
amount.value | string | Yes | Amount in minor units (e.g. "1000" for 10.00) |
amount.currency | string | Yes | ISO 4217 currency code (e.g. "EUR") |
terminal | object | Yes | Terminal information |
terminal.id | string | Yes | Terminal identifier |
terminal.type | string | Yes | Terminal type (e.g. "pos") |
terminal.terminal_entry_mode | string | Yes | How the card was read: chip, swipe, contactless, manual |
terminal.attended | boolean | Yes | Whether the terminal is attended by staff |
terminal.capabilities | array | Yes | Terminal capabilities (e.g. ["chip", "swipe", "contactless"]) |
track_2_data | string | No | Track 2 data from magnetic stripe read |
emv_data | string | No | EMV (chip) data in TLV format |
pin_block | object | No | Encrypted PIN block |
pin_block.block | string | No | The encrypted PIN block value |
cardholder_verification_method | string | No | CVM used (e.g. "pin", "signature", "no_cvm") |
stan | string | No | System Trace Audit Number |
authorization_type | string | No | Authorization type (e.g. "final", "pre_auth") |
pos_sequence_number | string | No | POS sequence number for the transaction |
Example Request Body
{
"card": {
"number": "4111111111111111",
"expiry_year": 2027,
"expiry_month": 12
},
"amount": {
"value": "1500",
"currency": "EUR"
},
"terminal": {
"id": "TERM-001",
"type": "pos",
"terminal_entry_mode": "chip",
"attended": true,
"capabilities": ["chip", "swipe", "contactless"]
},
"emv_data": "9F2608A1B2C3D4E5F6...",
"cardholder_verification_method": "pin",
"pin_block": {
"block": "0412345678ABCDEF"
},
"stan": "000123",
"authorization_type": "final"
}
Response
200 OK — MSK-encrypted envelope
The API returns an MSK envelope. Decrypt with terminal MSK to obtain the charge result:
{
"format": "TR31-DATA",
"ciphertext": "D4E5F6...",
"initialization_vector": "101112131415161718191A1B1C1D1E1F",
"mode": "CBC",
"key_kcv": "ABCD12"
}
Decrypted plaintext
{
"charge_id": "ch_abc123def456",
"status": "approved",
"amount": {
"value": "1500",
"currency": "EUR"
},
"gateway_data": {
"authorization_code": "A12345",
"rrn": "123456789012"
}
}
| Field | Type | Description |
|---|---|---|
charge_id | string | Unique identifier for the charge |
status | string | Charge status: approved, declined, pending |
amount | object | Confirmed charge amount |
amount.value | string | Amount in minor units |
amount.currency | string | ISO 4217 currency code |
gateway_data | object | Additional data from the payment gateway (may vary) |
If you send the same Idempotency-Key header for a request that was already processed, the API returns the original charge response without creating a duplicate. The Idempotency-Key header is echoed back in the response.
400 Bad Request
Returned when the request body is missing, malformed, or has invalid fields.
{
"error": {
"code": "4000",
"message": "Invalid request body",
"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 \
-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 "Idempotency-Key: idem-abc-123" \
-H "Content-Type: application/json" \
-d '{
"card": {
"number": "4111111111111111",
"expiry_year": 2027,
"expiry_month": 12
},
"amount": {
"value": "1500",
"currency": "EUR"
},
"terminal": {
"id": "TERM-001",
"type": "pos",
"terminal_entry_mode": "chip",
"attended": true,
"capabilities": ["chip", "swipe", "contactless"]
},
"emv_data": "9F2608A1B2C3D4E5F6...",
"cardholder_verification_method": "pin",
"pin_block": {
"block": "0412345678ABCDEF"
}
}'
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]
body = {
"card": {
"number": "4111111111111111",
"expiry_year": 2027,
"expiry_month": 12,
},
"amount": {"value": "1500", "currency": "EUR"},
"terminal": {
"id": terminal_id,
"type": "pos",
"terminal_entry_mode": "chip",
"attended": True,
"capabilities": ["chip", "swipe", "contactless"],
},
"emv_data": "9F2608A1B2C3D4E5F6...",
"cardholder_verification_method": "pin",
"pin_block": {"block": "0412345678ABCDEF"},
}
body_json = json.dumps(body, separators=(",", ":"))
signature_string = f"POST\n/pos/charge\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}",
"Idempotency-Key": "idem-abc-123",
"Content-Type": "application/json",
}
response = requests.post("https://api.viopay.io/pos/charge", headers=headers, data=body_json)
data = response.json()
print(f"Charge ID: {data['charge_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)
body = {
card: { number: "4111111111111111", expiry_year: 2027, expiry_month: 12 },
amount: { value: "1500", currency: "EUR" },
terminal: {
id: terminal_id, type: "pos", terminal_entry_mode: "chip",
attended: true, capabilities: ["chip", "swipe", "contactless"]
},
emv_data: "9F2608A1B2C3D4E5F6...",
cardholder_verification_method: "pin",
pin_block: { block: "0412345678ABCDEF" }
}
body_json = JSON.generate(body)
signature_string = "POST\n/pos/charge\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")
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["Idempotency-Key"] = "idem-abc-123"
request["Content-Type"] = "application/json"
request.body = body_json
response = http.request(request)
data = JSON.parse(response.body)
puts "Charge ID: #{data['charge_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 ChargeRequest struct {
Card Card `json:"card"`
Amount Amount `json:"amount"`
Terminal Terminal `json:"terminal"`
EmvData string `json:"emv_data"`
CVM string `json:"cardholder_verification_method"`
PinBlock PinBlock `json:"pin_block"`
}
type Card struct {
Number string `json:"number"`
ExpiryYear int `json:"expiry_year"`
ExpiryMonth int `json:"expiry_month"`
}
type Amount struct {
Value string `json:"value"`
Currency string `json:"currency"`
}
type Terminal struct {
ID string `json:"id"`
Type string `json:"type"`
TerminalEntryMode string `json:"terminal_entry_mode"`
Attended bool `json:"attended"`
Capabilities []string `json:"capabilities"`
}
type PinBlock struct {
Block string `json:"block"`
}
type ChargeResponse struct {
ChargeID string `json:"charge_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]
charge := ChargeRequest{
Card: Card{Number: "4111111111111111", ExpiryYear: 2027, ExpiryMonth: 12},
Amount: Amount{Value: "1500", Currency: "EUR"},
Terminal: Terminal{ID: terminalID, Type: "pos", TerminalEntryMode: "chip", Attended: true, Capabilities: []string{"chip", "swipe", "contactless"}},
EmvData: "9F2608A1B2C3D4E5F6...",
CVM: "pin",
PinBlock: PinBlock{Block: "0412345678ABCDEF"},
}
bodyBytes, _ := json.Marshal(charge)
sigString := fmt.Sprintf("POST\n/pos/charge\n\n%s\n%s\n%s\n%s", string(bodyBytes), terminalID, timestamp, nonce)
mac := hmac.New(sha256.New, hmacSecret)
mac.Write([]byte(sigString))
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
req, _ := http.NewRequest("POST", "https://api.viopay.io/pos/charge", 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("Idempotency-Key", "idem-abc-123")
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 ChargeResponse
json.Unmarshal(body, &result)
fmt.Printf("Charge ID: %s\n", result.ChargeID)
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 body = new
{
card = new { number = "4111111111111111", expiry_year = 2027, expiry_month = 12 },
amount = new { value = "1500", currency = "EUR" },
terminal = new
{
id = terminalId, type = "pos", terminal_entry_mode = "chip",
attended = true, capabilities = new[] { "chip", "swipe", "contactless" }
},
emv_data = "9F2608A1B2C3D4E5F6...",
cardholder_verification_method = "pin",
pin_block = new { block = "0412345678ABCDEF" }
};
var bodyJson = JsonSerializer.Serialize(body);
var sigString = $"POST\n/pos/charge\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");
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}");
request.Headers.Add("Idempotency-Key", "idem-abc-123");
var response = await client.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<JsonElement>(responseBody);
Console.WriteLine($"Charge ID: {data.GetProperty("charge_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));
$body = json_encode([
"card" => [
"number" => "4111111111111111",
"expiry_year" => 2027,
"expiry_month" => 12,
],
"amount" => ["value" => "1500", "currency" => "EUR"],
"terminal" => [
"id" => $terminalId,
"type" => "pos",
"terminal_entry_mode" => "chip",
"attended" => true,
"capabilities" => ["chip", "swipe", "contactless"],
],
"emv_data" => "9F2608A1B2C3D4E5F6...",
"cardholder_verification_method" => "pin",
"pin_block" => ["block" => "0412345678ABCDEF"],
]);
$sigString = "POST\n/pos/charge\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");
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}",
"Idempotency-Key: idem-abc-123",
"Content-Type: application/json",
],
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo "Charge ID: " . $data["charge_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 body = """
{
"card": {"number": "4111111111111111", "expiry_year": 2027, "expiry_month": 12},
"amount": {"value": "1500", "currency": "EUR"},
"terminal": {"id": "TERM-001", "type": "pos", "terminal_entry_mode": "chip", "attended": true, "capabilities": ["chip", "swipe", "contactless"]},
"emv_data": "9F2608A1B2C3D4E5F6...",
"cardholder_verification_method": "pin",
"pin_block": {"block": "0412345678ABCDEF"}
}""";
String sigString = "POST\n/pos/charge\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"))
.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("Idempotency-Key", "idem-abc-123")
.header("Content-Type", "application/json")
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
What's Next?
After creating a charge, you may need to:
- Refund the charge → Refund POS Charge
- Cancel the charge (void before settlement) → Cancel POS Charge