# Fuel forecourt

You don't know the amount until the nozzle stops. So authorize a ceiling, then capture what was
actually pumped.

A forecourt controller can't take a fixed amount up front. The customer authenticates for a maximum,
fuels, and you capture what they really dispensed — BaxPay releases the difference.

## Pre-auth → dispense → capture

**1. Pre-authorize a ceiling**

Open a session with `type: "PREAUTH"` and the ceiling as `amountMinor`, say €150. The shopper
authenticates at the pump terminal and BaxPay holds that amount on their funds.

**2. Release the pump**

When the session reads `AUTHORIZED` (or the `payment_session.preauthorized` webhook arrives), your
controller unlocks the nozzle. The hold reduces the shopper's available balance but has moved no
money.

**3. Capture the actual**

When fuelling ends, capture the real amount (≤ the ceiling). BaxPay takes that and **releases the
rest** of the hold in the same step.

- `POST /integration/sessions` — true
- `POST /integration/sessions/{id}/fuel/capture` — Idempotency-Key required
- `POST /integration/sessions/{id}/fuel/release` — no fuel taken

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/integration/sessions \
  -H "Authorization: Bearer $BAXPAY_CONNECTION" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "saleReferenceId": "site12-pump7-tx88",
    "type": "PREAUTH",
    "amountMinor": 15000,
    "currency": "EUR"
  }'
```

```json
{
  "success": true,
  "data": { "sessionId": "44e977a5-…", "type": "PREAUTH", "status": "CREATED", "amountMinor": 15000, "final": false }
}
```

In sandbox, play the shopper approving at the pump:

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/integration/sessions/44e977a5-…/simulate \
  -H "Authorization: Bearer $BAXPAY_CONNECTION" \
  -H "Content-Type: application/json" \
  -d '{ "outcome": "APPROVED" }'
```

```json
{ "success": true, "data": { "status": "AUTHORIZED", "final": false } }
```

The pump stops at €81.40. Capture that:

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/integration/sessions/44e977a5-…/fuel/capture \
  -H "Authorization: Bearer $BAXPAY_CONNECTION" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "finalAmountMinor": 8140 }'
```

```js
// Called by your forecourt controller when the nozzle is hung up.
async function captureFuel(sessionId, dispensedMinor, attemptKey) {
  const res = await fetch(`https://sandbox-api.baxpay.com/v1/integration/sessions/${sessionId}/fuel/capture`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.BAXPAY_CONNECTION}`,
      // Keep the same key if you retry this capture: it can never be booked twice.
      'Idempotency-Key': attemptKey,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ finalAmountMinor: dispensedMinor }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${body.message}`);
  return body.data;
}
```

```json
{
  "success": true,
  "data": {
    "sessionId": "44e977a5-…",
    "type": "PREAUTH",
    "status": "CAPTURED",
    "amountMinor": 15000,
    "capturedAmountMinor": 8140,
    "resultCode": "APPROVED",
    "final": true
  }
}
```

`amountMinor` stays the ceiling you authorized; `capturedAmountMinor` is what was charged. The
€68.60 difference went back to the shopper with the capture.

> **Never capture more than the ceiling**
> A capture above the pre-authorized `amountMinor` is refused with `400`. The shopper authenticated
> for that maximum, and the authentication is bound to it: charging beyond it would be exactly the
> replay the regulation exists to prevent. Size the ceiling generously up front.

## No fuel taken

The shopper authorized but drove off, or the nozzle never moved: **release** the pre-auth. The whole
hold goes back at once, the session becomes `VOIDED` and `payment_session.voided` fires. Voiding an
`AUTHORIZED` pre-auth (`/void`) does the same.

```bash
# sandbox: a second pre-auth, approved at the pump
SESSION=$(curl -s -X POST https://sandbox-api.baxpay.com/v1/integration/sessions \
  -H "Authorization: Bearer $BAXPAY_CONNECTION" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "saleReferenceId": "site12-pump7-tx89", "type": "PREAUTH", "amountMinor": 15000 }' | jq -r .data.sessionId)
curl -X POST "https://sandbox-api.baxpay.com/v1/integration/sessions/$SESSION/simulate" \
  -H "Authorization: Bearer $BAXPAY_CONNECTION" -H "Content-Type: application/json" \
  -d '{ "outcome": "APPROVED" }'
```

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/integration/sessions/44e977a5-…/fuel/release \
  -H "Authorization: Bearer $BAXPAY_CONNECTION"
```

```json
{
  "success": true,
  "data": { "status": "VOIDED", "resultCode": "ABORTED", "resultDetail": "Pre-authorization released", "final": true }
}
```

If you never capture or release, BaxPay releases the hold automatically **2 hours** after the
pre-auth was approved, with the same `payment_session.voided` event.

Prefer a release over capturing `0`: a zero capture is accepted, but it books a zero sale instead of
simply cancelling.

## IFSF and OPI estates

Forecourts speaking **IFSF** or **OPI** connect through the matching adapter with an `IFSF` or `OPI`
connection. The adapter maps their messages onto the same pre-auth / capture engine:

| Protocol | Endpoint | Maps to |
| --- | --- | --- |
| IFSF | `POST /integration/ifsf/reservation` | `MaxAmount` (major units) → pre-auth |
| IFSF | `POST /integration/ifsf/completion` | `FinalAmount` against `POITransactionID` → capture |
| OPI | `POST /integration/opi/cardServiceRequest` | Card service requests → sessions |

> **Same caveat as retail**
> These adapters are faithful message mappings, not certified device stacks. Certification is a
> separate effort: plan for it.

## Edge cases

- `Pump meter dispute`:
  The **captured** amount is authoritative for settlement. Reconcile pump totals against the
  [settlement batch](https://developers.baxpay.com/reconciliation), not against the pre-auth ceiling.
- `Capture retried`:
  Retry with the same `Idempotency-Key`. A capture on a session that is already `CAPTURED`
  answers `409`.
