What your application must survive
A hold outlives your process. The customer may pump for minutes. The terminal may be power-cycled, crash, or lose the network in between.
Write the rrn, approvalCode and transactionDate of an open pre-authorisation to durable storage before the nozzle is released, and recover them on startup. Every subsequent message needs them:
| Transaction | Request data |
|---|---|
| Pre Authorization | dateTimeStamp;authAmount;etpInput;ecrRefNo! |
| Pre Auth Extension | dateTimeStamp;rrn;transactionDate;approvalCode;etpInput;ecrRefNo! |
| Pre Auth Void | dateTimeStamp;authAmount;rrn;transactionDate;approvalCode;etpInput;ecrRefNo! |
| Purchase Advice / Capture | dateTimeStamp;amount;rrn;transactionDate;approvalCode;captureType;etpInput;ecrRefNo! |
| Partial Reversal | dateTimeStamp;amount;etpInput;ecrRefNo! |
| Suspect Reversal | dateTimeStamp;etpInput;ecrRefNo! |
The capture request
Capture is the one that settles the money, and it is worth setting out field by field. A
worked example, txType = 4:
130826111824;100;622523000317;130826;123456;1;0;12345678000234!
| # | Value | Field |
|---|---|---|
| 1 | 130826111824 | dateTimeStamp — now, as ddMMyyHHmmss |
| 2 | 100 | amount — the fuel actually delivered, in minor units. SAR 1.00 here |
| 3 | 622523000317 | rrn — from the pre-authorisation response, index 10 |
| 4 | 130826 | transactionDate — the pre-authorisation's date, ddMMyy |
| 5 | 123456 | approvalCode — from the pre-authorisation response, index 11 |
| 6 | 1 | Capture type — 1 is a final capture. See below |
| 7 | 0 | etpInput — the print flag. 0 disables printing |
| 8 | 12345678000234 | ecrRefNo — your own reference for the capture |
Fields 3, 4 and 5 all come from the pre-authorisation you are settling. That is the concrete
reason section 8 insists they are written to durable storage before the nozzle is released:
without all three, a delivered tank cannot be charged for.
Note the amount is the delivered figure, not the authorised one. Sending the authorised
amount back would charge every customer for a full tank regardless of what they took.
Final capture and partial capture
Field 6 selects between them:
| Field 6 | Capture type | In scope? |
|---|---|---|
1 | Final — settles the authorisation and closes it | Yes. The only value mada uses |
0 | Partial — settles part of the hold, leaving it open | No. Withdrawn under SAMA requirements |
On mada, field 6 is always 1. Partial capture is in the message format and is not
available to you: it has been withdrawn, and since SAMA is certifying mada only, there is no
current scenario in which 0 is correct. It is documented here so that a 0 seen in an older
integration or a vendor sample is recognised rather than copied.
Do not read that as "capture the full authorised amount". A final capture for less than the
hold is the ordinary outcome of almost every fill, and the issuer releases the difference; that
is what the SAMA framework means by "either equal to the pre-authorized amount or less". What
has gone is the mechanism for settling one authorisation in instalments, not the ability to
settle it for a smaller figure.
Field 7 is 0 on a forecourt. An AFD terminal has no printer. The customer's receipt is the
QR code on the authorisation screen, which is why that screen offers QR code or no
receipt and nothing else.
The partial reversal
Important
Visa only, and outside the current scope. This is how a Visa
authorisation is settled for less than the hold, because Visa does not accept a lesser
capture. With SAMA certifying mada only, nothing in your mada flow sends one.
Confirmed against production traffic, where the payment app logs the fields it parsed:
[, C6, 000000000002, 200526080254, 12345678000108, 0, <signature>, 120]
The request that produces it:
dateTimeStamp;amount;etpInput;ecrRefNo!
Structurally it is a pre-authorisation: an amount, a timestamp, your reference and the print
flag. No RRN, which means — like the suspect reversal below — it applies to the payment
app's last transaction rather than one you name, so ordering matters.
The amount is what was not dispensed
This is the trap, and it is the opposite of everything else in the guide.
A capture carries the fuel delivered. A partial reversal carries the fuel not delivered —
the unused part of the hold, which is released back to the cardholder.
| Authorised | Delivered | Amount in the message | |
|---|---|---|---|
| mada — Capture | SAR 300 | SAR 120 | 12000 — what was pumped |
| Visa — Partial Reversal | SAR 300 | SAR 120 | 18000 — what was not |
Same forecourt, same fill, two schemes, and the figure on the wire is inverted between them.
Reuse the capture's amount for a Visa partial reversal and you release the SAR 120 the customer
owes while charging them the SAR 180 of fuel they never took — and it will settle cleanly,
because both numbers are valid amounts against that hold.
Compute it as authorised − delivered, from the authorised figure you stored, and never by
reaching for the variable holding the capture amount.
The suspect reversal
Important
Descoped for mada. The suspect transaction is a Visa and Mastercard
mechanism, and with SAMA certifying mada only, it is not part of the current scope. Nothing
in your mada flow should send one. It is documented because the 120-second row of the void
matrix is where it belongs once other schemes come into scope, and because a terminal that
handles more than mada will meet it.
The message for the 120-second unknown timeout, where the terminal gives up with no idea what
happened. It is short:
110826121030;0;12345678000089!
dateTimeStamp, etpInput, ecrRefNo. That is all — no RRN and no amount.
That is not an omission, and it is the one thing to understand about this message. It carries no
reference because the payment app resolves the transaction itself, looking up its own record
of the last online transaction and taking the RRN and approval code from there. Which is the
only design that could work: a suspect reversal is sent precisely when you never received a
response, so you have no reference to quote.
Important
It follows that a suspect reversal is positional. It reverses whatever the
payment app did last, not a transaction you name. Send it before anything else transacts on
that terminal, and never as a tidy-up at the end of a shift — by then it will reverse the
wrong sale, and it will report success while doing so.
It is sent by the ordinary route — getPackData(reqData, 28, signature) and the same Intent
handoff as everything else. There is nothing special about the transport.
An authorisation your application has forgotten is money held against a customer's card that nothing will ever release. It will not fail loudly; it will sit there for 24 hours and then quietly expire, having taken fuel and charged nobody.
Reconcile what was never closed. Something has to find pre-authorisations with no capture and no void and resolve them. This is not the same problem as a failed sale on an attended terminal, where it is over inside a minute with a person standing there. A forecourt hold may need clearing hours later, on a terminal that has been power-cycled since, for a customer who left long ago.
Pump control is not in this protocol. Releasing and metering the nozzle is an FCS integration and has nothing to do with the ECR SDK. What the two systems must agree on is which authorisation belongs to which pump — and that mapping has to survive a restart of either side.
Updated 3 days ago
