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, telephone prepay, and bill payments, sent using the Esp:Merchandise element (not Esp:Transaction, which handles card PURCHASE). The worked example throughout is a ZESA prepaid electricity token purchase — the only VAS product implemented in this release. The same message flow and field rules are documented for telephone prepay, water, and bill payment products for completeness, but see the release scope note below before building against any of them.
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.
Release scopeOnly
ProductType=ELECTRICITY(ZESA) is implemented and supported in this release of the integration. WATER, TELEPHONE PREPAY, and BILL_PAYMENT are documented below for completeness and future extensibility, but must not be built against in this release — their field mappings (for example, the open question on carrying an account/subscriber reference in The MeterNumber omission rule) have not been confirmed for production use.
Supported VAS product types
Selecting a product category fully determines ExtendedTransactionType (defaulted, not hardcoded — see the field reference below), MerchantName, and ProductName — the latter two are not retailer-editable fields.
| Product category (till selection) | 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 |
TELEPHONE PREPAY | 9100 | ECONET | ECONET_AIRTIME | Mobile network prepaid airtime top-up |
BILL_PAYMENT | 9000 | DSTV | DSTV | Subscription / bill account payment |
The eSocket.POS developer guide documents exactly three ProductType values: TELEPHONE PREPAY, ELECTRICITY, and OTHER. Electricity and Telephone Prepay send that same value as ProductType. Water and Bill Payment have no ProductType value of their own — the reference implementation sends ProductType="OTHER" for both, and relies on ProductId, MerchantName, and ProductName to tell them apart. ExtendedTransactionType defaults to 9100 for Telephone Prepay and 9000 for everything else, but remains a configurable field (see the field reference below) — it is not hardcoded.
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.
TerminologyeSocket.POS documents two valid
Esp:Merchandiseflows for a purchase: a single-step Procure flow (Type="PROCURE", no separate confirmation message) and a two-step legacy Request/Confirm flow (Type="REQUEST", thenType="CONFIRM"). This guide and the reference implementation use the single-step Request/Procure flow throughout — treat any Request/Confirm-based documentation or examples you come across as describing that other, legacy flow, not this one.Whichever flow is used, the reversal phase is
Type="REVERSE"— this matches eSocket.POS's ownTypedefinition and its worked reversal examples. Some copies of the core XML Interface Quick Reference Guide's timeout-handling table instead show "REVERSAL" for this row; that conflicts with the documentedTypevalues and doesn't match any real example message, so treat it as an error in that table, not an alternative literal to send.For ZETDC specifically: the interface itself allows sending a REVERSE here — it isn't rejected — but ZESA discourages sending reversals for a completed token purchase and doesn't action them. See VAS REVERSE and Error handling and edge cases for the manual-intervention procedure to use instead.
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 | Configurable, defaulted per product category | 9000 / 9100 | Pre-filled per product category (see the product types table above) but left as a plain editable field — not hardcoded into the outgoing message. |
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 till's product category list above. eSocket.POS itself only recognises TELEPHONE PREPAY, ELECTRICITY, and OTHER as ProductType values; Water and Bill Payment categories are sent as ProductType="OTHER". |
CurrencyCode | Configurable / dynamic (PROCURE only) | 840 | ISO 4217 numeric, from the till's active currency — see the currency table below. Sent as a PosStructuredData element on PROCURE, not a top-level attribute — see CurrencyCode on the PROCURE message. |
TransactionAmount | Dynamic | 100 | Minor denomination units (cents). Sale amount entered by the cashier. |
UserId | Dynamic | 263773176741 | Subscriber/customer reference — e.g. mobile number for telephone prepay, 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,TELEPHONE PREPAY, 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 categories, use theUserIdattribute instead: eSocket.POS documentsUserIdas identifying the user in a merchandise request generally, and specifically as the MSISDN (phone number) for telephone prepay. This is documented for completeness only — per the release scope above,WATER,TELEPHONE PREPAY, andBILL_PAYMENTare not implemented in this release, and no real example message populatingUserIdfor a voucher/water/bill-payment purchase has been found to corroborate it. Confirm with EFT Corp before relying on it in a future release.
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 |
CurrencyCode on the PROCURE message
Esp:Merchandise does not declare a CurrencyCode attribute. Adding one directly to the element causes the interface to reject the message outright with a Format Error, not a decline — ResponseCode 30, MessageReasonCode 9791 ("Administrative response"), with the validator reporting Attribute "CurrencyCode" must be declared for element type "Esp:Merchandise". Do not add CurrencyCode as a top-level attribute on Esp:Merchandise.
Currency is instead supplied as a PosStructuredData element on the PROCURE message: <Esp:PosStructuredData Name="CurrencyCode" Value="{CODE}"/>, using the ISO 4217 numeric code (see the currency table above) of the currency tendered by the customer. This is the mechanism used by the reference implementation — see the worked PROCURE example in VAS PROCURE.
This value should match the currency the meter/account is actually billed in, as returned by the host in the REQUEST response's meterCurrency structured data (see Parsing the response). A mismatch between the tendered currency and the meter's billing currency is expected to be declined by the host — see the worked example below.
The reference implementation does not enforce this match before PROCURE: the tendered currency (selected by the operator) is sent as-is and is not compared against the REQUEST-returned meterCurrency before the purchase is allowed to proceed. Decide your own policy for production — lock the currency selector to meterCurrency once REQUEST returns it, or validate and block PROCURE on a mismatch before sending it.
CurrencyCode is not currently sent on REQUEST or REVERSE. REQUEST does not require it to succeed, and REVERSE is already documented as best-effort only for a merchant like ZETDC (see VAS REVERSE).
Example — a PROCURE declined for a currency mismatch:
<?xml version="1.0" encoding="UTF-8"?>
<Esp:Interface Version="1.0" xmlns:Esp="http://www.mosaicsoftware.com/Postilion/eSocket.POS/">
<Esp:Merchandise ActionCode="DECLINE" ResponseCode="05" RetrievalRefNr="260807143205"
TerminalId="TEST0001" TransactionAmount="100" LocalDate="0807" LocalTime="143209" Type="PROCURE">
<Esp:StructuredData Name="Narrative" Value="Invalid currency code / Currency mismatch"/>
</Esp:Merchandise>
</Esp:Interface>Treat a decline with this narrative as a currency-configuration problem to resolve before retrying, not a transient failure — confirm the tendered currency against the meter's meterCurrency first. See Error handling and edge cases.
Currency for this integration is controlled entirely through the CurrencyCode PosStructuredData value described above — do not try to work around a currency-mismatch decline by selecting a different ProductId for the same product; ProductId is not used as a per-currency selector in this implementation.
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 four 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:PosStructuredData Name="CurrencyCode" Value="{CURRENCY_CODE}"/> <!-- ISO 4217 numeric, tendered currency -->
</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:PosStructuredData Name="CurrencyCode" Value="840"/>
</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
TELEPHONE PREPAY,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.
REVERSE does not undo a completed ZETDC token purchaseOnce PROCURE succeeds, the token has already been drawn from the vendor's float and is not refunded by a subsequent REVERSE. Sending
Type="REVERSE"is still a valid message to send — it confirms what the host itself returns — but do not rely on it to claw back a completed purchase; treat it as best-effort only. See A REQUEST or PROCURE with an uncertain outcome for handling a REQUEST or PROCURE whose outcome is uncertain.
A REQUEST or PROCURE with an uncertain outcome
Esp:Merchandise supports exactly three message types: REQUEST, PROCURE, and REVERSE. There is no message type for querying or resending an earlier REQUEST or PROCURE. If one of these times out, or the connection drops before a response is received, your terminal has no way to independently confirm what happened upstream.
Do not retry, and do not rely on REVERSEDo not retry PROCURE for the same purchase in this situation — a token may already have been issued even though your terminal never received the response, and retrying risks drawing a second one. Do not rely on REVERSE to undo it either (see VAS REVERSE above). Hold the transaction as unconfirmed and escalate to EFT Corp for support in establishing the real outcome before concluding success, concluding failure, or refunding the customer.
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 (telephone prepay, 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, and see Error handling and edge cases below for what to do when a REQUEST or PROCURE times out with no response at all.
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. Decoded, but not printed on the dedicated ZESA receipt (see Receipt layout below) — the eVending SMS Token Layout spec's mandated receipt lines don't include a standalone rate line. Available if your integration wants to show it. |
| 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. Decoded and used — this is the source of the "Energy" line on the dedicated ZESA receipt. |
| 5 | tax_amount | 15.00 | Tax portion of the purchase. Decoded and used — this is the source of the "VAT" line on the dedicated ZESA receipt. |
| 6 | tax_rate | 0.15 | Tax rate applied. Decoded, but not printed, for the same reason as field 2 (rate) above. |
Multiple token records (Key Change Tokens)The
Tokenfield can contain more than one record, separated by#(a new Key Change Token is issued whenever the meter's encryption key group has expired). Split on#first, then split each record on|using the 7-field layout above. The last record's sub-fields are used for on-screen/receipt display (units, receipt number, net/tax amounts) — but every token number, in every record, must still be printed and loaded into the meter, in the order returned. When more than one record is present, print an explicit "Please input in the given order" instruction.
Rate and tax rate are decoded but not printedAll seven fields are decoded into a token record (token, units, rate, receipt number, net amount, tax amount, tax rate). Net amount and tax amount now feed the "Energy" and "VAT" lines on the dedicated ZESA receipt (see Receipt layout below). Rate and tax rate are parsed and available but still not printed, because the eVending spec's mandated receipt lines don't include a standalone rate or tax-rate line. If your market's fiscalisation rules require them, add your own line — don't assume this field selection is complete for your jurisdiction.
Reference decode logic:
tokenRecords = raw.Split('#') // one entry per Key Change Token
.Select(entry => entry.Split('|'))
.Where(p => p.Length >= 7)
.Select(p => new VasTokenRecord(
Token: p[0].Trim(), Units: p[1].Trim(), Rate: p[2].Trim(),
ReceiptNumber: p[3].Trim(),
NetAmountCents: ParseCents(p[4]), TaxAmountCents: ParseCents(p[5]),
TaxRate: p[6].Trim()))
last = tokenRecords[^1] // last record's sub-fields used for display
tokenDisplay = FormatTokenGroups(last.Token) // groups in 5s if exactly 20 digits, else raw
// Debt / REA totals use the same pipe layout, "#"-separated; sum the amount sub-field:
debtCents = SumPipeAmount(structuredData["Arrears"]) // description|receiptNr|amount|tax|balance
reaCents = SumPipeAmount(structuredData["FixedCharges"]) // description|receiptNr|amount|taxAmount|rate
Arrears (Debt) and FixedCharges (REA Levy) structured data
ZESA electricity purchases can carry two further pipe-delimited structured-data fields alongside Token, both using the same #-separated multi-record convention as Token — sum the amount sub-field across every record for the receipt line:
| Structured data Name | Format | Notes |
|---|---|---|
Arrears | description|receiptNumber|amount|tax|outstandingBalance | Outstanding debt recovered against this purchase, e.g. Debt Recovery|898766677|800|0|7700. Sum the amount sub-field for the receipt's "Debt" line — see Receipt layout below. |
FixedCharges | description|merchantReceiptNumber|amount|taxAmount|rate | REA (Rural Electrification Agency) levy, e.g. RE Levy(6%)|78|150|0|0.06. Sum the amount sub-field for the receipt's "REA" line — see Receipt layout below. |
Don't assume Arrears/FixedCharges will be populatedThese two fields are not always present in the PROCURE response, even on a purchase that did involve a non-zero debt recovery or REA levy. Treat their absence as zero, not as an error, but be aware the printed "Debt"/"REA" receipt lines may under-report the true amount collected when this happens. Confirm the current behaviour of these two fields with EFT Corp before relying on them being consistently present.
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. As of this revision, two distinct layouts exist, selected automatically by ProductType — see the routing note below.
Generic VAS receipt (WATER, TELEPHONE PREPAY, BILL_PAYMENT, and any Token-absent fallback):
| 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. |
ZESA-specific receipt (ProductType = ELECTRICITY):
| Line | Content | Source field |
|---|---|---|
| Header | Business name / "eSocket.POS" / date-time | Local system clock — not from the response. |
| Title | "ZESA TOKEN RECEIPT" | Static. |
| Terminal | Terminal ID | Configurable field, see the field reference above. |
| Date/Time | Transaction date/time, DD/MM/YY HH:mm | A shorter format than the generic receipt's DD/MM/YYYY HH:mm:ss, per the eVending spec. |
| RRN | Retrieval Reference Number | RetrievalRefNr echoed in the response. |
| TOKEN(S) block | Every token number, grouped in 5s, one per line, centred | Every record from Token, in order — see the token format above. Followed by "Please input in the given order" when more than one record is present. |
| Meter | Meter/account number | The value you sent. |
| KwH | Units purchased | Last token record's units sub-field. |
| Energy | Net electricity value, in the Settlement Currency (SCC) | Last token record's net amount. |
| Debt | Outstanding debt recovered, in SCC | Sum of Arrears amount sub-fields — 0.00 if the field is absent (see Arrears (Debt) and FixedCharges (REA Levy) structured data above). |
| REA | Rural Electrification Agency levy, in SCC | Sum of FixedCharges amount sub-fields — 0.00 if the field is absent. |
| VAT | Tax portion, in SCC | Last token record's tax amount. |
| Total Amt | Energy + Debt + REA + VAT, in SCC | Computed — not echoed by the host. |
| Amt Tend. | Amount the customer paid, in the Transaction Currency (TCC) | The response's TransactionAmount if parseable, else falls back to Total Amt. |
| Receipt | Utility-side receipt number | Last token record's receipt number sub-field. |
| (or) Result | Narrative text | Structured data Narrative, when there is no Token at all (e.g. a declined PROCURE) — same fallback rule as the generic layout. |
| Response | ResponseCode + description | e.g. "00 - Approved or completed successfully". |
| Footer | Thank-you / website | Static. |
Which layout prints, and how it's selectedThere is no separate "print token" button. The receipt builder inspects the last VAS product type: when it equals
ELECTRICITY(case-insensitive) it builds the ZESA-specific receipt above; for every other VAS product type it builds the generic receipt. Both are reached through the same "Print Last Receipt" button.
Settlement vs Transaction currency, and a CurrencyCode caveatEnergy/Debt/REA/VAT/Total Amt are labelled with the Settlement Currency Code (SCC) — structured data
SettlementCurrencyCodeif present, otherwise the response's echoedCurrencyCode, otherwise the currency the operator selected at request time. Amt Tend. is labelled with the Transaction Currency Code (TCC) — the response's echoedCurrencyCode, otherwise the request-time currency.In practice, expect both to fall back to the request-time currency: a live capture confirmed
Esp:Merchandiseresponses carry noCurrencyCodeattribute at all — the response'sCurrencyCodeis always null for VAS responses. Do not rely on parsing a currency out of a VAS response.
Sample printed output
Generic VAS receipt sample:
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
ZESA-specific receipt sample (ProductType = ELECTRICITY):
EFTCORP ZIMBABWE
eSocket.POS TEST
12/08/2026 14:57:29
ZESA TOKEN RECEIPT
----------------------------------------
Terminal : TEST0001
Date/Time : 12/08/26 14:57
RRN : 260807143205
----------------------------------------
TOKEN(S):
1234 5678 9012 3456 7890
Meter : 14316800680
KwH : 5.23
----------------------------------------
Energy : ZWG85.00
Debt : ZWG7.00
REA : ZWG1.50
VAT : ZWG15.00
----------------------------------------
Total Amt: ZWG108.50
Amt Tend.: ZWG108.50
----------------------------------------
Receipt : 123456789
Response : 00 - Approved or completed su
----------------------------------------
** 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. - Long value truncation: label/value formatting truncates — it does not wrap — once "label value" reaches the column width. This is confirmed behaviour, not a hypothetical: a full
ResponseCode00description on the ZESA receipt's "Response :" line is cut to 40 characters (see the sample above). Keep this in mind if you add your own label/value lines with longer values.
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 | Do not send a REVERSE, and do not retry PROCURE for the same purchase — a token may already have been drawn down even though your terminal never received the response, and Esp:Merchandise has no message type to independently confirm what happened upstream. Hold the transaction as unconfirmed and escalate to EFT Corp for support in establishing the real outcome before concluding failure or refunding the customer. |
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. Do not send a REVERSE either: the token has already been drawn down and, for a merchant like ZETDC, cannot actually be reversed — a REVERSE here would not undo it. If the token truly cannot be recovered, follow your organisation's manual token-recovery/void procedure with EFT Corp instead. |
| Attempting a REVERSE with no prior successful PROCURE | Disallow at the UI level — there is nothing upstream to reverse. Note this is about REQUEST-only lines specifically; even against a successfully procured line, REVERSE itself is best-effort only for a merchant like ZETDC. |
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. |
PROCURE declined with ResponseCode 05, narrative containing "Invalid currency code" / "Currency mismatch" | Currency-configuration problem, not a transient failure — do not retry unchanged. Correct the CurrencyCode value sent at PROCURE (see CurrencyCode on the PROCURE message) so it matches the REQUEST-returned meterCurrency, then re-attempt REQUEST. Do not try a different ProductId as a workaround — ProductId does not select currency in this implementation. |
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 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, telephone prepay, 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 18 days ago
