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 note

This guide does not repeat material already covered in the core eSocket.POS XML Interface Quick Reference Guide (message framing, Esp:Admin INIT/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.

ProductTypeExtendedTransactionTypeDefault MerchantNameDefault ProductNameTypical use case
ELECTRICITY9000ZETDCZETDC_PREPAIDZESA prepaid electricity token purchase
WATER9000HARAREHARAREMunicipal prepaid water token purchase
AIRTIME9100ECONETECONET_AIRTIMEMobile network prepaid airtime top-up
BILL_PAYMENT9000DSTVDSTVSubscription / 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:23001 over TCP. Confirm the actual host/port for your site before going live.
  • An initialised terminal (Esp:Admin INIT) — 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 ProductId values for each VAS product you will offer, issued by EFT Corp or the upstream VAS aggregator during onboarding (see the field reference below) — ProductId is 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:

  1. REQUEST — validates the meter/account and amount, and returns customer/account details for the cashier to confirm before money moves.
  2. PROCURE — commits the purchase. On success (ResponseCode 00) this is where the token (or a narrative result, for non-token products) is returned.
  3. 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 discrepancy

The core XML Interface Quick Reference Guide's Esp:Merchandise timeout table refers to this phase as "REVERSAL". The reference implementation sends Type="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:

  1. POS sends Esp:Merchandise Type="REQUEST" to eSocket.POS, which forwards it upstream to Postilion.
  2. Postilion returns the customer name and billing currency; the cashier confirms these with the customer.
  3. POS sends Esp:Merchandise Type="PROCURE" with the same RRN.
  4. Postilion returns the token (or narrative) with ActionCode="APPROVE" and ResponseCode="00".
  5. The till prints the receipt/token.
  6. 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 / ElementCategoryExample valueNotes
Esp:Interface VersionHardcoded1.0Fixed protocol version.
xmlns:Esp namespaceHardcodedhttp://www.mosaicsoftware.com/Postilion/eSocket.POS/Fixed.
Type (REQUEST / PROCURE / REVERSE)HardcodedREQUESTOne literal per phase; never user input.
ExtendedTransactionTypeHardcoded, derived9000 / 9100Looked up from ProductType — see the product types table above. Never let the retailer type this directly.
ApiProcessingCode (PROCURE only)HardcodedU50000Same literal for every PROCURE regardless of product type.
MerchantName (PROCURE only)Hardcoded per ProductTypeZETDCFixed lookup, not user-editable in the reference UI.
ProductName (PROCURE only)Hardcoded per ProductTypeZETDC_PREPAIDFixed lookup, paired with MerchantName.
TerminalIdConfigurableTEST0001Loaded once from eSocket.POS config, or entered once per session; same value the till already uses for card PURCHASE.
ProductIdConfigurable (per product)80000129Assigned 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.
ProductTypeConfigurable (selected)ELECTRICITYChosen from the fixed enumerated list above. Drives ExtendedTransactionType, MerchantName, and ProductName.
CurrencyCodeConfigurable / dynamic840ISO 4217 numeric, from the till's active currency — see the currency table below.
TransactionAmountDynamic100Minor denomination units (cents). Sale amount entered by the cashier.
UserIdDynamic263773176741Subscriber/customer reference — e.g. mobile number for airtime, account holder reference for bills.
MeterNumber (PosStructuredData)Dynamic14316800680ONLY transmitted when ProductType = ELECTRICITY (case-insensitive) — see below.
TransactionId (STAN)Dynamic, generated260755Generated per transaction by the POS — see the STAN/RRN section below for the uniqueness caveat.
RetrievalRefNr (RRN)Dynamic, generated260807143205Generated once at REQUEST and threaded unchanged through PROCURE and REVERSE for the same purchase attempt.
DateTimeDynamic, generated0807143205System clock, MMddHHmmss, regenerated fresh on every message — not user input, but not static either.

The MeterNumber omission rule

📘

Important

The reference implementation only emits the <Esp:PosStructuredData Name="MeterNumber"> element when ProductType is ELECTRICITY. For WATER, AIRTIME, and BILL_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 the UserId attribute 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)

CurrencyCode
USD840
ZWG924
ZMW967
ZAR710
MWK454
BWP072
ZIG932
GBP826
EUR978

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 products

For AIRTIME, WATER, and BILL_PAYMENT, the PROCURE response typically returns Esp:StructuredData Name="Narrative" instead of "Token" — a free-text confirmation string to print in place of a token block. Always check for Token first and fall back to Narrative (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 PROCURE

Retain the RRN, TransactionId, amount, ProductId, ProductType, UserId, and MeterNumber from a PROCURE only once that PROCURE has come back with ResponseCode 00 — 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

AttributeMeaning
ActionCodeAPPROVE / DECLINE / RETAIN / AUTH — check this first.
ResponseCodeISO-style 2-digit response code. 00 = approved. See Response code 09 below.
TypeEchoes the phase: REQUEST / PROCURE / REVERSE.
RetrievalRefNrEchoed RRN — confirm it matches what you sent.
TransactionAmountEchoed amount in minor units.
LocalDate + LocalTimeMMdd + 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 NamePresent at phaseMeaning
customerNameREQUESTRegistered account holder name for the meter/account — show to the cashier for verification before PROCURE.
meterCurrencyREQUESTCurrency the meter/account is billed in.
TokenPROCURE (success)The ZESA/VAS token payload — see the token format below.
NarrativePROCURE (success, non-token products)Free-text result to display/print when there is no token (airtime, water, bill payment).
📘

Check Token before Narrative

A response can in principle carry either field depending on the upstream product configuration — always check for Token first and only fall back to Narrative if no Token is present, regardless of ProductType.

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 #NameExampleNotes
0token_number12345678901234567890A 20-digit ZESA prepaid token in the standard format. Displayed grouped in blocks of 5 digits: 1234 5678 9012 3456 7890.
1units5.23kWh 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.
2rate0.1912Tariff rate applied. Not surfaced by the reference implementation's receipt, but present on the wire — consider printing it for customer transparency.
3receipt_number123456789ZETDC/utility-side receipt reference — print this alongside the token; customers may need it for queries with the utility.
4net_amount85.00Net electricity value before tax. Present on the wire, not printed by the reference implementation.
5tax_amount15.00Tax portion of the purchase. Present on the wire, not printed by the reference implementation.
6tax_rate0.15Tax rate applied. Present on the wire, not printed by the reference implementation.
📘

Do not silently discard fields 2 and 4–6

The 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:

LineContentSource field
HeaderBusiness name / "eSocket.POS" / date-timeLocal system clock — not from the response.
Title"VAS RECEIPT"Static.
TerminalTerminal IDConfigurable field, see the field reference above.
Date/TimeTransaction date/timeLocalDate + LocalTime from the PROCURE response, formatted DD/MM/YYYY HH:mm:ss.
RRNRetrieval Reference NumberRetrievalRefNr echoed in the response.
Product lineProductType (e.g. ELECTRICITY)The value you sent.
ProductProductIdThe value you sent.
Meter/AccMeter or account numberThe value you sent (even for products where it was not transmitted on the wire — print what the cashier entered).
CustomerAccount holder nameStructured data customerName, if present (typically only returned at REQUEST — carry it forward to print on the PROCURE receipt).
CurrencyBilling currencyStructured data meterCurrency, if present.
AmountSale amount, formatted with currency labelTransactionAmount ÷ 100, with the currency the till charged in.
TOKEN block20-digit token, grouped in 5s, centredDecoded from structured data Token, field 0 — see the token format above.
UnitskWh purchasedToken field 1.
ReceiptUtility-side receipt numberToken field 3.
(or) ResultNarrative textStructured data Narrative, when there is no Token (non-electricity products).
OutcomeActionCodeAPPROVE / DECLINE / etc.
ResponseResponseCode + descriptione.g. "00 - Approved or completed successfully".
FooterThank-you / websiteStatic.

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 = 40 in 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

DestinationMechanismNotes
Text fileWrite 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+ printerSend 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 controls

If 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

ConditionRequired handling
REQUEST or PROCURE times out with no responseSend a REVERSE using the same RRN/TransactionId/amount as the message that timed out.
REVERSE times out with no responseRepeat 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 PROCURENot a decline. Wait and resend the identical message (same RRN/TransactionId) after 180 seconds.
PROCURE approved but the printer fails before the token is printedDo 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 PROCUREDisallow at the UI level — there is nothing upstream to reverse.
MeterNumber left blank for a non-ELECTRICITY productThe 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 soonWill 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

  • customerName and UserId (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 way

Do 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, and ResponseCode for 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 Token value or customerName/UserId into 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/LocalTime fields 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 reading TerminalId from 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.

CodeMeaning
00Approved or completed successfully
05Do not honour
09Request in progress — resend after 180 seconds
12Invalid transaction
13Invalid amount
30Format error
51Not sufficient funds
91Issuer or switch inoperative
94Duplicate transaction
96System malfunction

Appendix B — glossary

TermMeaning
VASValue-Added Services — non-card purchases (electricity, water, airtime, bill payment) processed via Esp:Merchandise.
ZESAThe 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).
STANSystem Trace Audit Number — the TransactionId attribute; must be unique per terminal within the retention period.
RRNRetrieval Reference Number — correlates REQUEST, PROCURE, and REVERSE as one purchase attempt.
PosStructuredData / StructuredDataGeneric 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:MerchandiseThe XML element used for all VAS transactions, distinct from Esp:Transaction (used for card PURCHASE/DEPOSIT).

Did this page help you?