You pay at the pump, put in a few pounds of fuel, and your banking app shows £100 gone. Later it drops to the right amount. Plenty of people assume the forecourt has overcharged them, and a few ring their bank.
It's the system working as designed. That hold is a pay-at-pump pre-authorisation, and it's one of the most useful patterns in payments whenever the final amount isn't known up front. This post explains how it works at the pump, and then spends most of its time on the part I find more interesting: what happens when the link to the payment switch isn't there.
This is the first part of a series that follows on from my post on real-time integrations in fuel forecourt systems. At PDI I work on a service that sits between pay-at-pump terminals, pump controllers, tills and payment systems, and these posts are about the patterns that come out of that work. They stay at the level of industry concepts, so none of it is a tour of our internals.
Why you can't just charge for the fuel
The weak mental model is "charge the card for the fuel". It's how a shop works: scan the goods, total them up, take payment. It doesn't work at a pump, for one simple reason.
Nobody knows the amount until the fuel has flowed. And the pump has to be told how much it's allowed to dispense before any fuel flows at all. You can't let an unattended customer take an unlimited amount of fuel and then hope the card covers it.
So the payment has to happen in two halves. First, ask the card issuer to reserve an amount, and use that amount as a hard limit on the pump. Then, once the nozzle is back, charge the real amount and let the rest of the reservation go.
Hotels and car hire do exactly the same thing for exactly the same reason. The final bill isn't known at check-in, so they hold an amount and settle later. The pump just does it in minutes instead of days.
The better model: pre-authorise, limit, deliver, complete
Here's the flow at a pay-at-pump terminal, simplified to the steps that matter.
The card goes in. Our service reserves the pump so nobody else can take it. The terminal asks the payment switch for a pre-authorisation, say £100, and tells our service it was approved.
We authorise the pump with that amount as its limit, so it physically can't dispense more. The pump controller streams volume and price while the customer fuels. When the nozzle goes back, we read the finished sale and tell the terminal what was actually delivered.
The terminal then completes the payment with the switch for that real figure, and the hold drops to match. Once the terminal says the payment has cleared, we take the sale off the pump, record it and pass it on to the back office.
Here's the shape of our side of that, as illustrative C# with neutral names. It's simplified a long way, but the ordering is the point:
public async Task RunPayAtPumpSaleAsync(int pumpId, CancellationToken ct)
{
await _pumps.ReserveAsync(pumpId, ct);
// The terminal talks to the payment switch. We only learn the outcome
// and the approved limit, never the card number.
var approval = await _terminal.WaitForPreAuthAsync(pumpId, ct);
if (!approval.Approved)
{
await _pumps.ReleaseAsync(pumpId, ct);
return;
}
await _pumpController.AuthoriseAsync(pumpId, limit: approval.Amount, ct);
var sale = await _pumpController.WaitForSaleCompleteAsync(pumpId, ct);
// The terminal completes for the real amount, or reverses if it's zero.
await _terminal.SendDeliveredAmountAsync(pumpId, sale.Amount, ct);
await _terminal.WaitForPaymentClearedAsync(pumpId, ct);
await _pumps.CashOutAsync(pumpId, sale, ct);
}The card number never passes through our service at any point. We see that a payment was approved and the limit it came with, and we tell the terminal what was delivered. Everything to do with the card itself stays between the terminal and the switch.
The pump side has its own vocabulary
The pump controller has words of its own for this flow, and they're worth knowing because they show up in every log. We "claim" a pump by putting our till number on it, "cash out" to read the finished sale and remove it, and "release" to take our number off again.
Each kind of seller, the pay-at-pump terminal, the kiosk till and so on, has its own claim number. So the logs show at a glance who owned a pump at each step of a sale. When something goes wrong, "who held this pump, and when did they let go" is usually the first question, and those numbers answer it.
The limit is enforced by the pump controller, not by our service. We tell it the approved amount and it stops the pump there. That matters, because it means the most important safety rule in the flow doesn't depend on our service staying up.
Details people don't expect
A few things about this flow surprise people, including engineers.
The approval can carry restrictions. A card can be allowed some fuel grades and not others, and the pump gets limited accordingly, so the restriction is enforced at the nozzle rather than argued about afterwards.
A sale that delivers nothing isn't a zero-value completion. It goes to the switch as a reversal, which releases the whole hold. If you lift the nozzle and change your mind, that's the path you take.
And a £100 hold that becomes a charge of a few pounds is correct behaviour, not a bug. How quickly the leftover hold disappears from your banking app is up to your bank, not the forecourt. That's why people see it and worry.
When the link drops, it depends on the moment
The happy path is easy to tell. The part worth engineering for is what happens when the payment switch isn't answering, and the right answer depends entirely on when it stops.
I'll describe what the terminal does here, because most of this behaviour lives in the terminal's firmware, not in our service. I know it from reading that code and from site logs, not from building it.
Before the sale starts
If a pump's link to the switch can't sign in, the terminal switches the card reader off for that pump and shows an out-of-order screen. It also tells our service, so the fault shows up for the site staff. No card is ever taken that can't be authorised.
Each terminal holds one link to the switch per pump, which is why failure is per pump rather than per terminal. One pump can be out of order while its neighbour sells normally.
During the pre-authorisation
If the terminal asks for a pre-authorisation and no answer comes back in time, it sends a reversal, and the pump is never authorised. No authorisation, no fuel. The customer gets a cancelled sale, which is annoying, but nobody has been charged for fuel they didn't get, and nobody has fuel they didn't pay for.
If even the reversal can't be sent, the terminal stores it to send later and prints a cancelled receipt. The reversal isn't lost just because the link is.
After the fuel has flowed
This is the case that matters most. The customer has fuelled against a valid pre-authorisation, so as far as they're concerned the sale finishes normally.
If the completion can't reach the switch, it goes into a local store on the terminal. A background loop keeps trying to reconnect, and once the link is back it forwards the stored completions and reversals in turn. That's store-and-forward: accept the work locally, persist it, and deliver it when the other end is reachable again.
The pattern is familiar to anyone who's built an outbox in a .NET service. Here's the generic shape, not the terminal's actual code:
// Generic store-and-forward loop: persist first, forward when reachable.
while (!ct.IsCancellationRequested)
{
if (await _link.EnsureSignedInAsync(ct))
{
foreach (var pending in await _store.ReadPendingAsync(ct))
{
var result = await _link.SendAsync(pending, ct);
if (result.Accepted || result.RejectedPermanently)
await _store.RemoveAsync(pending.Id, ct); // never retry a hard reject
}
}
await Task.Delay(TimeSpan.FromSeconds(1), ct);
}The line I'd point anyone at is the permanent rejection. There's one case where retrying is pointless: the switch rejects a reversal outright. Retrying that forever just clogs the store, so the terminal prints the receipt and deletes the transaction instead.
Keep card data out of the middle
The design choice I like most in all of this is who doesn't see the card. The terminal and the switch handle card data. The orchestrating service in the middle, the one I work on, only ever sees approvals, limits and amounts.
That keeps our service far simpler than it would otherwise be. It doesn't store card numbers, doesn't log them, and can't leak them, because it never had them. When I read our logs, I'm reading pumps, limits and states, not anything a customer would worry about.
It also gives a clean answer to "what happens if our service falls over mid-sale". The payment itself is between the terminal and the switch, and the pump controller enforces the limit. Our service orchestrates the steps around the payment; it isn't the thing holding the money.
If you're designing anything that sits between a device and a payment provider, that's the pattern I'd copy. Let the device and the provider own the sensitive data, and let the middle own the workflow.
The kiosk is the contrast
Not every sale works like this. When a customer fuels first and pays inside, the terminal is a bystander. The pump is handed to the shop, the customer fuels and walks in, and the till takes payment through its own card reader.
That reader talks to the same payment switch. So when the switch is down, kiosk card payments fail too, but cash still works and the site can keep selling fuel. It's a useful reminder that the forecourt has more than one payment path, and they share some dependencies and not others.
Where it falls over
Most of the failure handling isn't ours. The out-of-order screen, the timed-out reversal, the store and the background forwarding all live in the terminal firmware and the switch. The orchestrating service does its part around them, but if you asked me to prove the store survives every kind of power cut, I'd be quoting someone else's code.
I also stop at the switch. What happens between the switch and the card issuer, settlement, chargebacks and how long a hold really lasts, I know only at the level any payments engineer would. I'm not going to pretend otherwise in a post about payments.
And store-and-forward moves risk rather than removing it. The customer's sale is safe, but the site is now carrying completions it hasn't been paid for yet. How long the store holds, and what happens if it fills or the terminal is replaced before it drains, is where the real risk sits.
Pre-authorise whenever the amount comes later
My position: pre-authorise then complete is the right pattern whenever the final amount isn't known until after the goods are handed over, and store-and-forward is what keeps it honest when the network doesn't cooperate. The £100 hold is the visible part of a design that protects both the customer and the site.
If you're building something similar, ask three questions early. What does the device do when the provider doesn't answer, at each moment of the flow? What gets stored, and what gets deliberately dropped? And which component in the middle can get away with never seeing the sensitive data at all?
Next in the series, I'll look at what happens when the pump side goes wrong instead: a sale that cashed out at nothing, and a stop command with a side effect nobody could see. The rest of my writing is on the blog.