VAS Developer Guide
Everything a till/POS integrator needs to build the eSocket.POS VAS interface: message flow, field rules, response parsing, receipt printing, and error handling
This guide covers Value-Added Services (VAS) transactions over the eSocket.POS interface — non-card purchases of utility tokens, airtime, and bill payments, sent using the Esp:Merchandise element (not Esp:Transaction, which handles card PURCHASE). The primary worked example throughout is a ZESA prepaid electricity token purchase, the highest-volume VAS product in the field, but the same message flow and field rules apply to airtime, water, and bill payment products.
Scope noteThis guide does not repeat material already covered in the core eSocket.POS XML Interface Quick Reference Guide (message framing,
Esp:AdminINIT/CLOSE,Esp:Error, general event/callback handling). Read that guide first if you have not integrated a card PURCHASE transaction before.
Supported VAS product types
Selecting a ProductType fully determines ExtendedTransactionType, MerchantName, and ProductName — these are not retailer-editable fields.
| ProductType | ExtendedTransactionType | Default MerchantName | Default ProductName | Typical use case |
|---|---|---|---|---|
ELECTRICITY | 9000 | ZETDC | ZETDC_PREPAID | ZESA prepaid electricity token purchase |
WATER | 9000 | HARARE | HARARE | Municipal prepaid water token purchase |
AIRTIME | 9100 | ECONET | ECONET_AIRTIME | Mobile network prepaid airtime top-up |
BILL_PAYMENT | 9000 | DSTV | DSTV | Subscription / bill account payment |
ExtendedTransactionType 9100 is reserved for telephone prepay vouchers (airtime); every other VAS product type routes on 9000.
Prerequisites
- eSocket.POS installed, configured, and reachable from the till — the reference implementation connects to
127.0.0.1:23001over TCP. Confirm the actual host/port for your site before going live. - An initialised terminal (
Esp:AdminINIT) — see the core XML Interface Quick Reference Guide, section 8. VAS transactions use the same initialised TCP session as card transactions; there is no separate VAS initialisation. - A Terminal ID provisioned in the eSocket.POS/Postilion database and enabled for the VAS product(s) you intend to sell.
- Valid
ProductIdvalues for each VAS product you will offer, issued by EFT Corp or the upstream VAS aggregator during onboarding (see the field reference below) —ProductIdis not a value you invent. - Access to the eSocket.POS developer's guide and the Postilion Merchandise interface specification for the authoritative upstream field definitions. This guide documents the subset exercised by the reference test harness.
Message overview
A VAS purchase is always a three-phase exchange, each phase a separate request/response pair over the same TCP session used for INIT:
- REQUEST — validates the meter/account and amount, and returns customer/account details for the cashier to confirm before money moves.
- PROCURE — commits the purchase. On success (
ResponseCode00) this is where the token (or a narrative result, for non-token products) is returned. - REVERSE — reverses a PROCURE that must be backed out (for example, a printer failure after purchase, or a POS timeout with no confirmed outcome). Only ever sent against a purchase that was successfully procured.
Naming discrepancyThe core XML Interface Quick Reference Guide's
Esp:Merchandisetimeout table refers to this phase as "REVERSAL". The reference implementation sendsType="REVERSE". Confirm with EFT Corp which literal your upstream Postilion Merchandise interface actually expects before going live — do not assume the two are interchangeable.
Sequence for a successful purchase:
- POS sends
Esp:Merchandise Type="REQUEST"to eSocket.POS, which forwards it upstream to Postilion. - Postilion returns the customer name and billing currency; the cashier confirms these with the customer.
- POS sends
Esp:Merchandise Type="PROCURE"with the same RRN. - Postilion returns the token (or narrative) with
ActionCode="APPROVE"andResponseCode="00". - The till prints the receipt/token.
- Only if the PROCURE must be backed out: POS sends
Esp:Merchandise Type="REVERSE"with the same RRN, amount, and product/meter/user values as the PROCURE.
Framing
VAS messages use the same TCP length-prefixed framing as every other eSocket.POS message: a 2-byte big-endian length header for messages under 65,535 bytes, or 0xFF 0xFF followed by a 4-byte length for larger messages. See EspFrame.Encode in the reference implementation, and section 7 of the core XML Interface Quick Reference Guide.
Field reference — hardcoded, configurable, and dynamic fields
Every attribute in a VAS message falls into one of three categories. Getting this classification right is the single most important part of a correct integration:
- Hardcoded — a literal constant your integration should never make user-editable. Wrong values here typically cause silent misrouting upstream, not a clean decline.
- Configurable — set once per deployment (per terminal/till), not per transaction. Store in your POS configuration, not in transaction-entry code.
- Dynamic — must be supplied fresh by your till/POS integration for every transaction, either from cashier/customer input or generated at send-time.
| Attribute / Element | Category | Example value | Notes |
|---|---|---|---|
Esp:Interface Version | Hardcoded | 1.0 | Fixed protocol version. |
xmlns:Esp namespace | Hardcoded | http://www.mosaicsoftware.com/Postilion/eSocket.POS/ | Fixed. |
Type (REQUEST / PROCURE / REVERSE) | Hardcoded | REQUEST | One literal per phase; never user input. |
ExtendedTransactionType | Hardcoded, derived | 9000 / 9100 | Looked up from ProductType — see the product types table above. Never let the retailer type this directly. |
ApiProcessingCode (PROCURE only) | Hardcoded | U50000 | Same literal for every PROCURE regardless of product type. |
MerchantName (PROCURE only) | Hardcoded per ProductType | ZETDC | Fixed lookup, not user-editable in the reference UI. |
ProductName (PROCURE only) | Hardcoded per ProductType | ZETDC_PREPAID | Fixed lookup, paired with MerchantName. |
TerminalId | Configurable | TEST0001 | Loaded once from eSocket.POS config, or entered once per session; same value the till already uses for card PURCHASE. |
ProductId | Configurable (per product) | 80000129 | Assigned by EFT Corp/upstream aggregator per VAS product offered at onboarding — not invented by the retailer. Pre-fill per ProductType in your POS config; do not free-type in production. |
ProductType | Configurable (selected) | ELECTRICITY | Chosen from the fixed enumerated list above. Drives ExtendedTransactionType, MerchantName, and ProductName. |
CurrencyCode | Configurable / dynamic | 840 | ISO 4217 numeric, from the till's active currency — see the currency table below. |
TransactionAmount | Dynamic | 100 | Minor denomination units (cents). Sale amount entered by the cashier. |
UserId | Dynamic | 263773176741 | Subscriber/customer reference — e.g. mobile number for airtime, account holder reference for bills. |
MeterNumber (PosStructuredData) | Dynamic | 14316800680 | ONLY transmitted when ProductType = ELECTRICITY (case-insensitive) — see below. |
TransactionId (STAN) | Dynamic, generated | 260755 | Generated per transaction by the POS — see the STAN/RRN section below for the uniqueness caveat. |
RetrievalRefNr (RRN) | Dynamic, generated | 260807143205 | Generated once at REQUEST and threaded unchanged through PROCURE and REVERSE for the same purchase attempt. |
DateTime | Dynamic, generated | 0807143205 | System clock, MMddHHmmss, regenerated fresh on every message — not user input, but not static either. |
The MeterNumber omission rule
ImportantThe reference implementation only emits the
<Esp:PosStructuredData Name="MeterNumber">element whenProductTypeisELECTRICITY. ForWATER,AIRTIME, andBILL_PAYMENT, the meter/account number field is still required as UI input (so the cashier cannot submit a blank), but it is silently dropped and never sent in the XML. If your integration needs an account/meter/subscriber reference transmitted for those other product types, use theUserIdattribute instead — confirm the correct field with EFT Corp per product, since this is a product-specific upstream mapping decision, not a fixed protocol rule.
STAN and RRN generation — build your own, do not copy the reference logic verbatim
The reference implementation generates the Transaction ID (STAN) as DateTime.Now.ToString("ymmss") and the RRN as DateTime.Now.ToString("yyMMddHHmmss"), both purely time-based with no persistence or collision check. This is adequate for a manual test tool sending one transaction at a time, but it is not a safe pattern for a production POS:
- The core XML Interface Quick Reference Guide requires Transaction IDs to be 6-digit numbers that do not begin with zero, and to be unique within the eSocket.POS transaction retention period. A single
"y"format specifier does not guarantee a fixed 2-digit year component in all years, and a purely clock-derived value can collide if two transactions are sent within the same second. - Implement a persisted, monotonically increasing (or otherwise collision-free) STAN generator per terminal, respecting the no-leading-zero rule, and re-use the exact original STAN/RRN pair when resending an in-progress VAS message (see Response code 09 below).
- Generate the RRN once at REQUEST time and carry the same value through PROCURE and REVERSE for that purchase attempt — the upstream host needs this to correlate all three phases as one purchase. The reference implementation does this via a form-level field; your integration must persist this per in-flight transaction, not per UI session.
Currency codes (ISO 4217 numeric)
| Currency | Code |
|---|---|
| USD | 840 |
| ZWG | 924 |
| ZMW | 967 |
| ZAR | 710 |
| MWK | 454 |
| BWP | 072 |
| ZIG | 932 |
| GBP | 826 |
| EUR | 978 |
Message definitions
VAS REQUEST
Sent first, for every VAS product. Validates the meter/account and returns customer details for cashier confirmation. No money moves at this phase.
<?xml version="1.0" encoding="UTF-8"?>
<Esp:Interface Version="1.0" xmlns:Esp="http://www.mosaicsoftware.com/Postilion/eSocket.POS/">
<Esp:Merchandise DateTime="{DATETIME}" ExtendedTransactionType="{EXT_TX_TYPE}"
ProductId="{PRODUCT_ID}" ProductType="{PRODUCT_TYPE}"
RetrievalRefNr="{RRN}" TerminalId="{TERMINAL_ID}"
TransactionAmount="{AMOUNT_MINOR}" TransactionId="{TRANSACTION_ID}"
UserId="USER_ID" Type="REQUEST">
<Esp:PosStructuredData Name="MeterNumber" Value="{METER_NUMBER}"/> <!-- ELECTRICITY only -->
</Esp:Merchandise>
</Esp:Interface>Worked example — ZESA electricity meter, TEST/UAT values:
<?xml version="1.0" encoding="UTF-8"?>
<Esp:Interface Version="1.0" xmlns:Esp="http://www.mosaicsoftware.com/Postilion/eSocket.POS/">
<Esp:Merchandise DateTime="0807143205" ExtendedTransactionType="9000" ProductId="80000129"
ProductType="ELECTRICITY" RetrievalRefNr="260807143205" TerminalId="TEST0001"
TransactionAmount="100" TransactionId="614532" UserId="263773176741" Type="REQUEST">
<Esp:PosStructuredData Name="MeterNumber" Value="14316800680"/>
</Esp:Merchandise>
</Esp:Interface>Response — approved: returns the account holder's name and billing currency for the cashier to confirm before proceeding to PROCURE.
<?xml version="1.0" encoding="UTF-8"?>
<Esp:Interface Version="1.0" xmlns:Esp="http://www.mosaicsoftware.com/Postilion/eSocket.POS/">
<Esp:Merchandise ActionCode="APPROVE" ResponseCode="00" RetrievalRefNr="260807143205"
TerminalId="TEST0001" TransactionAmount="100" Type="REQUEST">
<Esp:StructuredData Name="customerName" Value="J MOYO"/>
<Esp:StructuredData Name="meterCurrency" Value="USD"/>
</Esp:Merchandise>
</Esp:Interface>VAS PROCURE
Commits the purchase using the same RRN, TransactionId, ProductId, ProductType, UserId, MeterNumber, and TransactionAmount as the REQUEST (a fresh STAN and DateTime are still generated for this second message). Adds three PosStructuredData elements that are only present at PROCURE.
<?xml version="1.0" encoding="UTF-8"?>
<Esp:Interface Version="1.0" xmlns:Esp="http://www.mosaicsoftware.com/Postilion/eSocket.POS/">
<Esp:Merchandise DateTime="{DATETIME}" ExtendedTransactionType="{EXT_TX_TYPE}"
ProductId="{PRODUCT_ID}" ProductType="{PRODUCT_TYPE}"
RetrievalRefNr="{RRN}" TerminalId="{TERMINAL_ID}"
TransactionAmount="{AMOUNT_MINOR}" TransactionId="{TRANSACTION_ID}"
UserId="USER_ID" Type="PROCURE">
<Esp:PosStructuredData Name="MeterNumber" Value="{METER_NUMBER}"/> <!-- ELECTRICITY only -->
<Esp:PosStructuredData Name="MerchantName" Value="{MERCHANT_NAME}"/> <!-- hardcoded per ProductType -->
<Esp:PosStructuredData Name="ProductName" Value="{PRODUCT_NAME}"/> <!-- hardcoded per ProductType -->
<Esp:PosStructuredData Name="ApiProcessingCode" Value="U50000"/> <!-- hardcoded, always -->
</Esp:Merchandise>
</Esp:Interface>Worked example continued:
<?xml version="1.0" encoding="UTF-8"?>
<Esp:Interface Version="1.0" xmlns:Esp="http://www.mosaicsoftware.com/Postilion/eSocket.POS/">
<Esp:Merchandise DateTime="0807143207" ExtendedTransactionType="9000" ProductId="80000129"
ProductType="ELECTRICITY" RetrievalRefNr="260807143205" TerminalId="TEST0001"
TransactionAmount="100" TransactionId="614532" UserId="263773176741" Type="PROCURE">
<Esp:PosStructuredData Name="MeterNumber" Value="14316800680"/>
<Esp:PosStructuredData Name="MerchantName" Value="ZETDC"/>
<Esp:PosStructuredData Name="ProductName" Value="ZETDC_PREPAID"/>
<Esp:PosStructuredData Name="ApiProcessingCode" Value="U50000"/>
</Esp:Merchandise>
</Esp:Interface>Response — approved: the Token field is what your POS must decode and print (see The ZESA / VAS token format).
<?xml version="1.0" encoding="UTF-8"?>
<Esp:Interface Version="1.0" xmlns:Esp="http://www.mosaicsoftware.com/Postilion/eSocket.POS/">
<Esp:Merchandise ActionCode="APPROVE" ResponseCode="00" RetrievalRefNr="260807143205"
TerminalId="TEST0001" TransactionAmount="100" LocalDate="0807" LocalTime="143209" Type="PROCURE">
<Esp:StructuredData Name="Token" Value="12345678901234567890|5.23|0.1912|123456789|85.00|15.00|0.15"/>
</Esp:Merchandise>
</Esp:Interface>
Non-token productsFor
AIRTIME,WATER, andBILL_PAYMENT, the PROCURE response typically returnsEsp:StructuredData Name="Narrative"instead of"Token"— a free-text confirmation string to print in place of a token block. Always check forTokenfirst and fall back toNarrative(see Parsing the response below).
VAS REVERSE
Sent only against a purchase that has already been PROCUREd — never send a REVERSE for a REQUEST that was never PROCUREd. Repeats the same attribute set as the original PROCURE, with Type="REVERSE" and the same RRN/TransactionId/amount/product/meter/user values as that PROCURE.
<?xml version="1.0" encoding="UTF-8"?>
<Esp:Interface Version="1.0" xmlns:Esp="http://www.mosaicsoftware.com/Postilion/eSocket.POS/">
<Esp:Merchandise DateTime="0807143301" ExtendedTransactionType="9000" ProductId="80000129"
ProductType="ELECTRICITY" RetrievalRefNr="260807143205" TerminalId="TEST0001"
TransactionAmount="100" TransactionId="614532" UserId="263773176741" Type="REVERSE">
<Esp:PosStructuredData Name="MeterNumber" Value="14316800680"/>
</Esp:Merchandise>
</Esp:Interface>
Only reverse a confirmed PROCURERetain the RRN,
TransactionId, amount,ProductId,ProductType,UserId, andMeterNumberfrom a PROCURE only once that PROCURE has come back withResponseCode00— do not offer a reversal option for a PROCURE that is still in-flight or that failed outright, since there is nothing upstream yet to reverse.
Parsing the response
Top-level attributes
| Attribute | Meaning |
|---|---|
ActionCode | APPROVE / DECLINE / RETAIN / AUTH — check this first. |
ResponseCode | ISO-style 2-digit response code. 00 = approved. See Response code 09 below. |
Type | Echoes the phase: REQUEST / PROCURE / REVERSE. |
RetrievalRefNr | Echoed RRN — confirm it matches what you sent. |
TransactionAmount | Echoed amount in minor units. |
LocalDate + LocalTime | MMdd + HHmmss — concatenate to get a 10-digit MMddHHmmss timestamp for display formatting. |
StructuredData / PosStructuredData — parse by walking every descendant
The fields that actually matter for VAS — the token, the customer name, the currency, the narrative — are not top-level attributes. They arrive as child elements, either Esp:StructuredData or Esp:PosStructuredData, each carrying a Name/Value attribute pair. Do not assume a fixed nesting depth or a fixed element name: walk every descendant element of the response and collect any node whose local name is StructuredData or PosStructuredData into a name→value lookup (case-insensitive names):
foreach (node in every descendant element of the Esp:Merchandise response)
if node.LocalName in ("StructuredData", "PosStructuredData"):
name = node.Attributes["Name"]
value = node.Attributes["Value"]
if name and value:
result[name] = value # case-insensitive key
| Structured data Name | Present at phase | Meaning |
|---|---|---|
customerName | REQUEST | Registered account holder name for the meter/account — show to the cashier for verification before PROCURE. |
meterCurrency | REQUEST | Currency the meter/account is billed in. |
Token | PROCURE (success) | The ZESA/VAS token payload — see the token format below. |
Narrative | PROCURE (success, non-token products) | Free-text result to display/print when there is no token (airtime, water, bill payment). |
Check Token before NarrativeA response can in principle carry either field depending on the upstream product configuration — always check for
Tokenfirst and only fall back toNarrativeif noTokenis present, regardless ofProductType.
Response code 09 — asynchronous "in progress"
Handle this explicitly
ResponseCode"09"("Request in progress") on a VAS REQUEST or PROCURE means the upstream VAS host has not yet returned a final outcome. Do not treat this as a decline and do not treat it as a success — the reference implementation resends the same message (same RRN/TransactionId) after 180 seconds. Build this wait-and-resend behaviour into your integration rather than surfacing an immediate error to the cashier.
For all other non-approved outcomes, follow the general Esp:Merchandise timeout/error handling rules in the core XML Interface Quick Reference Guide: a REQUEST or PROCURE that times out with no response at all should be reversed; a REVERSE/REVERSAL that times out should be repeated indefinitely, or until an operational limit is reached, with the transaction logged for manual exception handling.
The ZESA / VAS token format
The raw value of the Token structured-data field is a 7-field, pipe-delimited string:
token_number|units|rate|receipt_number|net_amount|tax_amount|tax_rate
| Field # | Name | Example | Notes |
|---|---|---|---|
| 0 | token_number | 12345678901234567890 | A 20-digit ZESA prepaid token in the standard format. Displayed grouped in blocks of 5 digits: 1234 5678 9012 3456 7890. |
| 1 | units | 5.23 | kWh purchased. Printed exactly as returned — no unit label is added automatically; add "kWh" yourself when formatting for display if the host does not include it. |
| 2 | rate | 0.1912 | Tariff rate applied. Not surfaced by the reference implementation's receipt, but present on the wire — consider printing it for customer transparency. |
| 3 | receipt_number | 123456789 | ZETDC/utility-side receipt reference — print this alongside the token; customers may need it for queries with the utility. |
| 4 | net_amount | 85.00 | Net electricity value before tax. Present on the wire, not printed by the reference implementation. |
| 5 | tax_amount | 15.00 | Tax portion of the purchase. Present on the wire, not printed by the reference implementation. |
| 6 | tax_rate | 0.15 | Tax rate applied. Present on the wire, not printed by the reference implementation. |
Do not silently discard fields 2 and 4–6The reference test harness only decodes and displays fields 0, 1, and 3 (token, units, receipt number). Fields 2 and 4–6 (rate, net amount, tax amount, tax rate) are present in every token response but are currently dropped. Decide deliberately whether your retailer-facing receipt should include them — many retailers are contractually required to itemise tax on a fiscalised receipt, so do not assume the reference implementation's field selection is a complete requirement for your integration.
Reference decode logic:
parts = raw.Split('|')
tokenRaw = parts[0].Trim()
tokenDisplay = tokenRaw.Length == 20
? "{p0} {p1} {p2} {p3} {p4}" // grouped in 5s, e.g. 1234 5678 9012 3456 7890
: tokenRaw // fall back to raw string if not exactly 20 digits
units = parts.Length > 1 ? parts[1].Trim() : "-"
receiptNr = parts.Length > 3 ? parts[3].Trim() : "-"
// rate = parts[2], netAmount = parts[4], taxAmount = parts[5], taxRate = parts[6] if you need them
Printing the token / VAS receipt
Receipt layout
The reference implementation prints a fixed-width, 40-column plain-text receipt suitable for a thermal POS printer. Build your receipt from the fields below, in this order:
| Line | Content | Source field |
|---|---|---|
| Header | Business name / "eSocket.POS" / date-time | Local system clock — not from the response. |
| Title | "VAS RECEIPT" | Static. |
| Terminal | Terminal ID | Configurable field, see the field reference above. |
| Date/Time | Transaction date/time | LocalDate + LocalTime from the PROCURE response, formatted DD/MM/YYYY HH:mm:ss. |
| RRN | Retrieval Reference Number | RetrievalRefNr echoed in the response. |
| Product line | ProductType (e.g. ELECTRICITY) | The value you sent. |
| Product | ProductId | The value you sent. |
| Meter/Acc | Meter or account number | The value you sent (even for products where it was not transmitted on the wire — print what the cashier entered). |
| Customer | Account holder name | Structured data customerName, if present (typically only returned at REQUEST — carry it forward to print on the PROCURE receipt). |
| Currency | Billing currency | Structured data meterCurrency, if present. |
| Amount | Sale amount, formatted with currency label | TransactionAmount ÷ 100, with the currency the till charged in. |
| TOKEN block | 20-digit token, grouped in 5s, centred | Decoded from structured data Token, field 0 — see the token format above. |
| Units | kWh purchased | Token field 1. |
| Receipt | Utility-side receipt number | Token field 3. |
| (or) Result | Narrative text | Structured data Narrative, when there is no Token (non-electricity products). |
| Outcome | ActionCode | APPROVE / DECLINE / etc. |
| Response | ResponseCode + description | e.g. "00 - Approved or completed successfully". |
| Footer | Thank-you / website | Static. |
Sample printed output
EFTCORP ZIMBABWE
eSocket.POS TEST
07/08/2026 14:32:09
VAS RECEIPT
----------------------------------------
Terminal : TEST0001
Date/Time : 07/08/2026 14:32:09
RRN : 260807143205
----------------------------------------
ELECTRICITY
Product : 80000129
Meter/Acc : 14316800680
Customer : J MOYO
Currency : USD
----------------------------------------
Amount: USD $ 1.00
----------------------------------------
TOKEN:
1234 5678 9012 3456 7890
Units : 5.23
Receipt : 123456789
----------------------------------------
Outcome : APPROVE
Response : 00 - Approved or completed successfully
----------------------------------------
** THANK YOU **
www.eftcorp.com
Layout mechanics (for a fixed-width thermal receipt)
- Column width: 40 characters (
W = 40in the reference implementation) — adjust to your printer's actual character width, but keep it fixed for the whole receipt. - Centred lines (header, title, token block, footer): pad text with spaces on both sides so it sits in the middle of the line; truncate rather than wrap if the text is longer than the column width.
- Label/value lines (Amount, Outcome, Response, Units, Receipt): left-align the label, right-align the value, padding the middle with spaces so the value's last character lands on the final column.
- Horizontal rules: a full line of
-characters (40 dashes) used as section separators.
Output destinations
| Destination | Mechanism | Notes |
|---|---|---|
| Text file | Write the formatted receipt text to a timestamped .txt file, UTF-8 encoded, in a dedicated receipts folder. | Useful for logging/archival and for environments without an attached printer (e.g. UAT). Reference path pattern: receipt_yyyyMMdd_HHmmss.txt. |
| Windows/GDI+ printer | Send the same text through a PrintDocument to any installed Windows printer by name (or the system default if left blank). | Reference implementation uses Courier New at 7.5pt with a 13px line height (≈ 60 lines per A4 page). For a narrow thermal roll, set the correct paper width in the printer driver, not in application code. |
Fiscalisation / duplicate-print controlsIf your market requires fiscalised receipts or controls on reprinting financial documents, apply those controls at the point you call your print routine (e.g. mark the transaction as "printed", require a supervisor override to reprint) — the eSocket.POS interface itself has no concept of a receipt and will not prevent you from printing the same token twice if you call your print function twice.
Error handling and edge cases
| Condition | Required handling |
|---|---|
| REQUEST or PROCURE times out with no response | Send a REVERSE using the same RRN/TransactionId/amount as the message that timed out. |
| REVERSE times out with no response | Repeat the REVERSE indefinitely (or up to an operational limit), then log for manual/offline exception handling. Never abandon an unconfirmed REVERSE silently. |
ResponseCode 09 on REQUEST or PROCURE | Not a decline. Wait and resend the identical message (same RRN/TransactionId) after 180 seconds. |
| PROCURE approved but the printer fails before the token is printed | Do not attempt a second PROCURE for the same purchase — recover the token from your stored response and reprint. If the token cannot be recovered, follow your organisation's manual token-recovery/void procedure with EFT Corp before considering a REVERSE, since the token itself has already been issued. |
| Attempting a REVERSE with no prior successful PROCURE | Disallow at the UI level — there is nothing upstream to reverse. |
MeterNumber left blank for a non-ELECTRICITY product | The reference implementation still requires a non-blank entry even though it is not transmitted — decide whether your integration keeps this validation or relaxes it per product. |
Duplicate TransactionId/RRN reused too soon | Will be rejected or misrouted upstream. Implement a robust, collision-free generator — do not reuse the reference implementation's pure clock-based approach unmodified in production. |
Compliance and security considerations
VAS/Merchandise transactions carry no cardholder data (no PAN, track data, or PIN), so they do not themselves extend PCI-DSS cardholder data environment (CDE) scope. However, the surrounding integration still touches sensitive data and must be built to the same security bar as the card PURCHASE flow.
Data minimisation and PII
customerNameandUserId(typically a phone number/subscriber ID) returned in VAS responses are personal data. Do not log them in plaintext to shared/long-retention logs; mask or omit them from any log destined for a wider audience, and apply your organisation's data retention policy to stored receipts.- Do not embed real customer meter numbers, phone numbers, or names in test scripts, sample code, or support tickets shared outside the immediate integration team — use TEST/UAT values for all development and demonstration purposes.
- The decoded token itself is effectively a bearer instrument (whoever holds the 20-digit number can redeem the electricity units) — treat printed and stored tokens with the same handling discipline as a gift voucher or cash equivalent, not as routine transaction metadata.
Transport and network security
eSocket.POS always runs on the same physical machine as the retail POS/till application it serves — this is a fixed deployment constraint, not an assumption. The reference implementation reflects this by connecting to 127.0.0.1:23001, i.e. loopback only. Because this traffic never leaves the machine, it never traverses a network segment, so TLS is not applicable to this hop and is not required for it.
Loopback-only by design — keep it that wayDo not bind or expose the eSocket.POS TCP listener on a network-reachable interface, and do not reconfigure a till to reach eSocket.POS over the LAN "for convenience" (e.g. a shared eSocket.POS instance for multiple lanes). The entire security model here depends on this channel staying local-machine-only — any deployment that breaks that constraint reintroduces an unencrypted-transport gap (SOC 2 Confidentiality / ISO/IEC 27001) that this architecture is otherwise not exposed to. Treat "same machine, always" as a control to be enforced (e.g. firewall the port to loopback), not just a fact about today's topology.
- Restrict any other process or account on that same machine from reaching the loopback port unnecessarily (principle of least privilege) — co-location narrows the network attack surface to nil, but does not by itself limit local access.
- The terminal configuration files read by the POS (
configagent_properties.txt,properties.txt) should be protected with file-system permissions restricting write access to the eSocket.POS service account/administrators only.
Audit logging (PCI-DSS Requirement 10 / ISO 27001 alignment)
- Log, at minimum:
TerminalId,TransactionId(STAN),RetrievalRefNr,ProductType,ProductId,DateTime,ActionCode, andResponseCodefor every REQUEST/PROCURE/REVERSE — this gives a complete, time-synchronised audit trail without needing to log the token or customer PII. - Do not log the raw
Tokenvalue orcustomerName/UserIdinto general application logs; if you need them for support/reconciliation, store them in a separate, access-controlled record with defined retention, not in free-text debug logs. - Ensure the POS till's system clock is time-synchronised (e.g. NTP) with eSocket.POS — the
DateTime/LocalDate/LocalTimefields in this interface depend on it, and audit trail integrity requires consistent timestamps across systems.
Secure coding
- Validate and sanitise all cashier-entered dynamic fields (amount, meter number, user ID, product ID) before building the XML — the message builders in the reference implementation perform no XML-escaping of these values; a meter number or narrative value containing XML special characters (
<,&,") could malform the outgoing message. Escape or reject invalid input at the point of entry. - Never hardcode a production
TerminalId, host, or port into a compiled binary shared across sites — load them from a per-deployment configuration file or secure configuration store, consistent with the existing pattern of readingTerminalIdfrom eSocket.POS's own configuration. - Handle every network and parse failure gracefully (connection refused, timeout, malformed XML) without crashing the till application or leaking stack traces/internal paths to the cashier UI.
Appendix A — key VAS response codes
This is the subset most relevant to VAS. Refer to the core XML Interface Quick Reference Guide / upstream ISO 8583 field 39 table for the complete list.
| Code | Meaning |
|---|---|
| 00 | Approved or completed successfully |
| 05 | Do not honour |
| 09 | Request in progress — resend after 180 seconds |
| 12 | Invalid transaction |
| 13 | Invalid amount |
| 30 | Format error |
| 51 | Not sufficient funds |
| 91 | Issuer or switch inoperative |
| 94 | Duplicate transaction |
| 96 | System malfunction |
Appendix B — glossary
| Term | Meaning |
|---|---|
| VAS | Value-Added Services — non-card purchases (electricity, water, airtime, bill payment) processed via Esp:Merchandise. |
| ZESA | The Zimbabwe Electricity Supply Authority; "ZESA token" is the common name for a prepaid electricity token, issued in this interface under ProductType ELECTRICITY via ZETDC (Zimbabwe Electricity Transmission and Distribution Company). |
| STAN | System Trace Audit Number — the TransactionId attribute; must be unique per terminal within the retention period. |
| RRN | Retrieval Reference Number — correlates REQUEST, PROCURE, and REVERSE as one purchase attempt. |
| PosStructuredData / StructuredData | Generic Name/Value child elements used to carry fields that do not have a dedicated XML attribute (meter number, merchant/product name, token, narrative, customer name, currency). |
| Esp:Merchandise | The XML element used for all VAS transactions, distinct from Esp:Transaction (used for card PURCHASE/DEPOSIT). |
Updated about 5 hours ago
