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 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.

📘

Release scope

Only 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)ExtendedTransactionTypeDefault MerchantNameDefault ProductNameTypical use case
ELECTRICITY9000ZETDCZETDC_PREPAIDZESA prepaid electricity token purchase
WATER9000HARAREHARAREMunicipal prepaid water token purchase
TELEPHONE PREPAY9100ECONETECONET_AIRTIMEMobile network prepaid airtime top-up
BILL_PAYMENT9000DSTVDSTVSubscription / 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: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.
📘

Terminology

eSocket.POS documents two valid Esp:Merchandise flows for a purchase: a single-step Procure flow (Type="PROCURE", no separate confirmation message) and a two-step legacy Request/Confirm flow (Type="REQUEST", then Type="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 own Type definition 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 documented Type values 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:

  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.
ExtendedTransactionTypeConfigurable, defaulted per product category9000 / 9100Pre-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)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 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".
CurrencyCodeConfigurable / dynamic (PROCURE only)840ISO 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.
TransactionAmountDynamic100Minor denomination units (cents). Sale amount entered by the cashier.
UserIdDynamic263773176741Subscriber/customer reference — e.g. mobile number for telephone prepay, 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, TELEPHONE PREPAY, 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 categories, use the UserId attribute instead: eSocket.POS documents UserId as 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, and BILL_PAYMENT are not implemented in this release, and no real example message populating UserId for 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)

CurrencyCode
USD840
ZWG924
ZMW967
ZAR710
MWK454
BWP072
ZIG932
GBP826
EUR978

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 products

For TELEPHONE PREPAY, 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.

📘

REVERSE does not undo a completed ZETDC token purchase

Once 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 REVERSE

Do 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

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 (telephone prepay, 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, 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 #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. 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.
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. Decoded and used — this is the source of the "Energy" line on the dedicated ZESA receipt.
5tax_amount15.00Tax portion of the purchase. Decoded and used — this is the source of the "VAT" line on the dedicated ZESA receipt.
6tax_rate0.15Tax rate applied. Decoded, but not printed, for the same reason as field 2 (rate) above.
📘

Multiple token records (Key Change Tokens)

The Token field 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 printed

All 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 NameFormatNotes
Arrearsdescription|receiptNumber|amount|tax|outstandingBalanceOutstanding 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.
FixedChargesdescription|merchantReceiptNumber|amount|taxAmount|rateREA (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 populated

These 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):

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.

ZESA-specific receipt (ProductType = ELECTRICITY):

LineContentSource field
HeaderBusiness name / "eSocket.POS" / date-timeLocal system clock — not from the response.
Title"ZESA TOKEN RECEIPT"Static.
TerminalTerminal IDConfigurable field, see the field reference above.
Date/TimeTransaction date/time, DD/MM/YY HH:mmA shorter format than the generic receipt's DD/MM/YYYY HH:mm:ss, per the eVending spec.
RRNRetrieval Reference NumberRetrievalRefNr echoed in the response.
TOKEN(S) blockEvery token number, grouped in 5s, one per line, centredEvery 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.
MeterMeter/account numberThe value you sent.
KwHUnits purchasedLast token record's units sub-field.
EnergyNet electricity value, in the Settlement Currency (SCC)Last token record's net amount.
DebtOutstanding debt recovered, in SCCSum of Arrears amount sub-fields — 0.00 if the field is absent (see Arrears (Debt) and FixedCharges (REA Levy) structured data above).
REARural Electrification Agency levy, in SCCSum of FixedCharges amount sub-fields — 0.00 if the field is absent.
VATTax portion, in SCCLast token record's tax amount.
Total AmtEnergy + Debt + REA + VAT, in SCCComputed — 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.
ReceiptUtility-side receipt numberLast token record's receipt number sub-field.
(or) ResultNarrative textStructured data Narrative, when there is no Token at all (e.g. a declined PROCURE) — same fallback rule as the generic layout.
ResponseResponseCode + descriptione.g. "00 - Approved or completed successfully".
FooterThank-you / websiteStatic.
📘

Which layout prints, and how it's selected

There 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 caveat

Energy/Debt/REA/VAT/Total Amt are labelled with the Settlement Currency Code (SCC) — structured data SettlementCurrencyCode if present, otherwise the response's echoed CurrencyCode, otherwise the currency the operator selected at request time. Amt Tend. is labelled with the Transaction Currency Code (TCC) — the response's echoed CurrencyCode, otherwise the request-time currency.

In practice, expect both to fall back to the request-time currency: a live capture confirmed Esp:Merchandise responses carry no CurrencyCode attribute at all — the response's CurrencyCode is 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 = 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.
  • 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 ResponseCode 00 description 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

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 responseDo 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 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. 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 PROCUREDisallow 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 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.
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

  • 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 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, telephone prepay, 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?