Skip to main content

Create POS Charge

POST /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-Key header

Authentication

POS HMAC Signature — requires device identification headers and HMAC signature for every request.

Device Identification Headers

HeaderRequiredFormatDescription
X-Terminal-IdYesTerminal ID stringIdentifier of the terminal processing the charge. Example: TERM-001
User-AgentYesViopay-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-TypeYesposClient type identifier. Always pos for POS devices.

Signature Headers

HeaderRequiredFormatDescription
X-Key-IdYesFree-form stringIdentifier of the HMAC key used to generate the signature. Example: hmac-key-2025-01
X-TimestampYesRFC3339Current time in UTC. Must be within 5 minutes of server time. Example: 2025-12-13T14:25:30Z
X-NonceYesAlphanumeric stringUnique random value for each request. At least 12 characters. Example: a1b2c3d4e5f6
X-SignatureYesv1={base64_hmac}HMAC-SHA256 signature over the request. See signature computation below.

Optional Headers

HeaderRequiredDescription
Idempotency-KeyNoUnique 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}
ComponentDescription
methodHTTP method — always POST
pathRequest path — /pos/charge
queryQuery string (empty for this endpoint)
bodyThe raw JSON request body (MSK envelope, not plaintext)
terminal_idValue of X-Terminal-Id header
timestampValue of X-Timestamp header
nonceValue 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

FieldTypeRequiredDescription
cardobjectYesCard details
card.numberstringYesCard PAN (Primary Account Number)
card.expiry_yearintegerYesCard expiry year (4 digits)
card.expiry_monthintegerYesCard expiry month (1–12)
amountobjectYesCharge amount
amount.valuestringYesAmount in minor units (e.g. "1000" for 10.00)
amount.currencystringYesISO 4217 currency code (e.g. "EUR")
terminalobjectYesTerminal information
terminal.idstringYesTerminal identifier
terminal.typestringYesTerminal type (e.g. "pos")
terminal.terminal_entry_modestringYesHow the card was read: chip, swipe, contactless, manual
terminal.attendedbooleanYesWhether the terminal is attended by staff
terminal.capabilitiesarrayYesTerminal capabilities (e.g. ["chip", "swipe", "contactless"])
track_2_datastringNoTrack 2 data from magnetic stripe read
emv_datastringNoEMV (chip) data in TLV format
pin_blockobjectNoEncrypted PIN block
pin_block.blockstringNoThe encrypted PIN block value
cardholder_verification_methodstringNoCVM used (e.g. "pin", "signature", "no_cvm")
stanstringNoSystem Trace Audit Number
authorization_typestringNoAuthorization type (e.g. "final", "pre_auth")
pos_sequence_numberstringNoPOS 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"
}
}
FieldTypeDescription
charge_idstringUnique identifier for the charge
statusstringCharge status: approved, declined, pending
amountobjectConfirmed charge amount
amount.valuestringAmount in minor units
amount.currencystringISO 4217 currency code
gateway_dataobjectAdditional data from the payment gateway (may vary)
Idempotency

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 -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"
}
}'

What's Next?

After creating a charge, you may need to: