Skip to main content

End-to-End Example

Complete walkthrough of a POS device from first boot through a card payment. Use this page as the integration checklist; each step links to the detailed API reference.

Actors

ActorRole
POS appRuns on the terminal (PAX, Sunmi, etc.)
Merchant portalBackoffice UI — merchant claims the device with a pairing code
VioPay APIhttps://api.viopay.io

Sample identifiers

ItemExample value
Device IDPOS-8F3A2C91
Terminal IDTID-004812
Merchant ID550e8400-e29b-41d4-a716-446655440000
Attestation tokenatt_f83kLmP9s2ab
Charge IDch_abc123def456

Sequence diagram


Phase 1 — Device pairing (plaintext)

Step 1 — Bootstrap

POST /pos/bootstrap
X-Device-Id: POS-8F3A2C91
User-Agent: Viopay-POS/1.4.2 (Android 12; PAX-A920)
X-Client-Type: pos
{
"status": "ok",
"server_time": "2025-12-13T14:20:00Z",
"min_supported_version": "1.3.5"
}

Step 2 — Pairing code

POST /pos/pairing-code
(same activation headers)
{
"pairing_code": "A3K9F2",
"expires_in_sec": 300
}

Show A3K9F2 on the terminal screen. Merchant enters it in the portal.

Step 3 — Poll status

GET /pos/status
{
"device_status": "inactive",
"is_claimed": false,
"pairing_code_status": "pending"
}

Repeat until is_claimed is true and device_status is claimed.

Step 4 — Claim (merchant portal)

POST /pos/claim
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json

{
"pairing_code": "A3K9F2",
"device_id": "POS-8F3A2C91"
}

Phase 2 — Attestation & keys

Step 5 — Attest

Sign device_id\nkey_id\ntimestamp\nnonce with the device identity HMAC secret.

POST /pos/attest
Authorization: (not used — signature headers instead)
X-Device-Id: POS-8F3A2C91
X-Key-Id: device-id-key-2025-01
X-Timestamp: 2025-12-13T14:25:30Z
X-Nonce: a1b2c3d4e5f6
X-Signature: v1=SGVsbG8gV29ybGQ=
{
"status": "attested",
"attestation_token": "att_f83kLmP9s2ab",
"expires_in_sec": 300
}
Token lifetime

Complete steps 6–8 within 5 minutes while the attestation token is valid.

Step 6 — Download keys

POST /pos/keys
Authorization: Attestation att_f83kLmP9s2ab
X-Device-Id: POS-8F3A2C91

Response (abbreviated — values are TR-31 key blocks):

{
"transport": { "format": "TR31", "encryption": "AES-256", "wrapped_under": "kek" },
"keys": {
"tmk": { "key_block": "...", "usage": "key-encryption" },
"kek": { "key_block": "...", "usage": "key-encryption" },
"msk": { "key_block": "...", "usage": "data-encryption" },
"tpk": { "key_block": "...", "usage": "pin-encryption" },
"tdk": { "key_block": "...", "usage": "data-encryption" },
"mac": { "key_block": "...", "usage": "message-authentication" }
},
"verification": { "kcv": { "tmk": "A1B2C3", "kek": "B2C3D4", "msk": "C3D4E5", ... } }
}

Install order on device:

ZMK (pre-provisioned) → unwrap TMK
TMK → unwrap KEK
KEK → unwrap MSK, TPK, TDK, MAC

Verify each key with its KCV before continuing.


Phase 3 — MSK-protected config & activation

From this point, sensitive JSON travels inside an MSK envelope (TR31-DATA).

MSK envelope format

{
"format": "TR31-DATA",
"ciphertext": "<hexBinary>",
"initialization_vector": "<hexBinary, 16 bytes>",
"mode": "CBC",
"key_kcv": "C3D4E5"
}
FieldNotes
ciphertextAES-256-CBC over hex-encoded UTF-8 JSON plaintext
initialization_vector16 random bytes, hex-encoded; API generates IV on responses
modeAlways CBC
key_kcvPresent on API responses; optional on POS requests

Step 7a — Config (core, MSK-encrypted)

POST /pos/config
Authorization: Attestation att_f83kLmP9s2ab
X-Device-Id: POS-8F3A2C91

Response (wire): MSK envelope — decrypt locally to obtain core config JSON (no terminal_parameters).

Decrypted plaintext (example):

{
"terminal": { "terminal_id": "TID-004812", "device_id": "POS-8F3A2C91" },
"merchant": { "merchant_key": "mk_acme", "name": "Acme Corp", "tip_info": {}, "tax_info": {}, "surcharge_info": {} },
"emv": { "aid_list": ["A0000000031010"], "terminal_capabilities": "E0F0C8" },
"security": { "pin_required": true },
"branding": { "theme": "dark" },
"device_config": { "heartbeat_interval_sec": 60 }
}

Store terminal.terminal_id — required as X-Terminal-Id for charge requests.

See Config for the full field reference.

Step 7b — Terminal parameters (MSK-encrypted, gzip inside)

POST /pos/terminal-parameters
Authorization: Attestation att_f83kLmP9s2ab
X-Device-Id: POS-8F3A2C91

Response (wire): MSK envelope — decrypt locally (multiple chunks in production), gunzip inner bytes, assign JSON to terminal_parameters.

{
"format": "TR31-DATA-CHUNKED",
"content_encoding": "gzip",
"mode": "CBC",
"key_kcv": "C3D4E5",
"chunks": [
{
"ciphertext": "A1B2C3...",
"initialization_vector": "000102030405060708090A0B0C0D0E0F",
"mode": "CBC"
}
]
}

See Terminal Parameters.

Step 8 — Activate

Plaintext request (encrypt with MSK before sending):

{
"emv_ready": true,
"keys_installed": true,
"network_ok": true
}
POST /pos/activate
Authorization: Attestation att_f83kLmP9s2ab
X-Device-Id: POS-8F3A2C91
Content-Type: application/json

{ "format": "TR31-DATA", "ciphertext": "...", "initialization_vector": "...", "mode": "CBC" }

Decrypted response:

{
"status": "activated",
"activated_at": "2025-12-13T14:30:00Z"
}

Device is now ready for payments.


Phase 4 — MSK-protected payments

Auth switches to HMAC over the full HTTP request (including the MSK envelope body).

HMAC signature string

POST
/pos/charge

{raw_envelope_json_body}
TID-004812
2025-12-13T14:35:00Z
f9e8d7c6b5a4

Compute HMAC-SHA256(signature_string, mac_secret) → Base64 → prefix with v1=.

important

Sign the encrypted envelope JSON, not the plaintext charge object.

Step 9 — Create charge

Plaintext (encrypt with MSK):

{
"card": { "number": "4111111111111111", "expiry_year": 2027, "expiry_month": 12 },
"amount": { "value": "1500", "currency": "EUR" },
"terminal": {
"id": "TID-004812",
"type": "pos",
"terminal_entry_mode": "chip",
"attended": true,
"capabilities": ["chip", "contactless"]
},
"emv_data": "9F2608A1B2C3D4E5F6...",
"cardholder_verification_method": "pin",
"pin_block": { "block": "0412345678ABCDEF" },
"stan": "000123",
"authorization_type": "final"
}
POST /pos/charge
X-Terminal-Id: TID-004812
X-Timestamp: 2025-12-13T14:35:00Z
X-Nonce: f9e8d7c6b5a4
X-Key-Id: hmac-key-2025-01
X-Signature: v1=dGVzdC1zaWduYXR1cmU=
X-Client-Type: pos
User-Agent: Viopay-POS/1.4.2 (Android 12; PAX-A920)
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{ "format": "TR31-DATA", "ciphertext": "...", "initialization_vector": "...", "mode": "CBC" }

Decrypted response:

{
"charge_id": "ch_abc123def456",
"status": "approved",
"amount": { "value": "1500", "currency": "EUR" },
"gateway_data": { "authorization_code": "A12345", "rrn": "123456789012" }
}

See Create POS Charge.

Step 10 — Refund (optional)

Plaintext request:

{
"amount": { "value": "1500", "currency": "EUR" },
"reason": "customer_request"
}
POST /pos/charge/ch_abc123def456/refund
(HMAC headers + MSK envelope body)

Decrypted response:

{
"refund_id": "rf_xyz789",
"status": "approved",
"amount": { "value": "1500", "currency": "EUR" }
}

Step 11 — Cancel / reverse (optional)

No request body. HMAC signs empty body.

POST /pos/charge/ch_abc123def456/cancel
(HMAC headers, no body)

Decrypted response:

{
"charge_id": "ch_abc123def456",
"status": "cancelled"
}

/pos/charge/:chargeID/reverse behaves identically to /cancel.


MSK encrypt/decrypt on POS (pseudocode)

After unwrapping MSK from the TR-31 block delivered in step 6:

function mskEncrypt(plaintextJson: String): Envelope {
iv = randomBytes(16)
// Encrypt hex(utf8(json)) with AES-256-CBC using terminal MSK
ciphertext = AES_CBC_ENCRYPT(mskKey, iv, hexEncode(plaintextJson))
return {
format: "TR31-DATA",
ciphertext: hexEncode(ciphertext),
initialization_vector: hexEncode(iv),
mode: "CBC"
}
}

function mskDecrypt(envelope: Envelope): String {
plaintextHex = AES_CBC_DECRYPT(mskKey, hexDecode(envelope.iv), hexDecode(envelope.ciphertext))
return utf8Decode(hexDecode(plaintextHex))
}

The API decrypts or encrypts the same envelope format server-side using the terminal's installed keys.


API summary — where MSK is used

EndpointMSK on requestMSK on responseAuth
POST /pos/bootstrapPOS Activation
POST /pos/pairing-codePOS Activation
GET /pos/statusPOS Activation
POST /pos/claimBearer JWT
POST /pos/attestPOS Activation + HMAC
POST /pos/keysAttestation
POST /pos/configAttestation
POST /pos/terminal-parameters✅ (gzip, chunked)Attestation
POST /pos/activateAttestation
POST /pos/chargeHMAC
POST /pos/charge/:id/refundHMAC
POST /pos/charge/:id/cancelHMAC
POST /pos/charge/:id/reverseHMAC

Key usage at runtime

KeyUsed for
MSKCore config, terminal parameters, activate, charge API payloads
TPKPIN block encryption inside charge plaintext
TMAC / HMAC secretHTTP request authentication (charge/refund/cancel)
TDKAuxiliary data encryption (legacy; MSK preferred for API payloads)
KEK / TMKKey transport only (TR-31 unwrap at install time)