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
| Actor | Role |
|---|---|
| POS app | Runs on the terminal (PAX, Sunmi, etc.) |
| Merchant portal | Backoffice UI — merchant claims the device with a pairing code |
| VioPay API | https://api.viopay.io |
Sample identifiers
| Item | Example value |
|---|---|
| Device ID | POS-8F3A2C91 |
| Terminal ID | TID-004812 |
| Merchant ID | 550e8400-e29b-41d4-a716-446655440000 |
| Attestation token | att_f83kLmP9s2ab |
| Charge ID | ch_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
}
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"
}
| Field | Notes |
|---|---|
ciphertext | AES-256-CBC over hex-encoded UTF-8 JSON plaintext |
initialization_vector | 16 random bytes, hex-encoded; API generates IV on responses |
mode | Always CBC |
key_kcv | Present 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=.
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
| Endpoint | MSK on request | MSK on response | Auth |
|---|---|---|---|
POST /pos/bootstrap | — | — | POS Activation |
POST /pos/pairing-code | — | — | POS Activation |
GET /pos/status | — | — | POS Activation |
POST /pos/claim | — | — | Bearer JWT |
POST /pos/attest | — | — | POS Activation + HMAC |
POST /pos/keys | — | — | Attestation |
POST /pos/config | — | ✅ | Attestation |
POST /pos/terminal-parameters | — | ✅ (gzip, chunked) | Attestation |
POST /pos/activate | ✅ | ✅ | Attestation |
POST /pos/charge | ✅ | ✅ | HMAC |
POST /pos/charge/:id/refund | ✅ | ✅ | HMAC |
POST /pos/charge/:id/cancel | — | ✅ | HMAC |
POST /pos/charge/:id/reverse | — | ✅ | HMAC |
Key usage at runtime
| Key | Used for |
|---|---|
| MSK | Core config, terminal parameters, activate, charge API payloads |
| TPK | PIN block encryption inside charge plaintext |
| TMAC / HMAC secret | HTTP request authentication (charge/refund/cancel) |
| TDK | Auxiliary data encryption (legacy; MSK preferred for API payloads) |
| KEK / TMK | Key transport only (TR-31 unwrap at install time) |
Related pages
- Overview — flow diagram and header reference
- Keys — TR-31 key hierarchy and KCV verification
- Config · Terminal Parameters · Activate
- Create POS Charge · Refund · Cancel