Payments & settlement

How the back-office payments API exposes CDR payment status, multi-leg settlement details, recovery actions, VAT and commission configuration, and invoice generation.

Model

A payment in this API is a CDR viewed from the billing side. The payment id is the CDR id. Two independent status values describe it:

ValueWhereMeaning
paymentStatusGET /backoffice/payments, GET /backoffice/payments/{id}Business status of the CDR payment. Exists for every CDR.
processingStatusGET /backoffice/payments/cdrs/{cdrId}/settlement-detailsState of the asynchronous settlement record. Exists only when the async settlement engine handled the CDR.

A settlement collects the CDR amount through one or more legs (for example a preauthorization capture, an off-session card charge, a prepaid-wallet debit). Each leg has one or more attempts. A successful leg is a contribution. Later attempts only ask for the remaining amount, so collected money is never requested twice.

Payment status

paymentStatus values returned by the list and details endpoints:

paymentStatusMeaning
PENDINGCDR received, payment not finished yet (includes deferred payments).
PAIDPayment collected. Also returned when payment was skipped for a CDR that is not free of charge.
PAYMENT_SKIPPEDNo payment taken because the charge was free of charge (private pool flagged FREE_OF_CHARGE).
FAILEDPayment failed. The CDR can be exempted or recovered.
EXEMPTEDAn operator wrote the debt off (POST /backoffice/payments/exempt/{cdrId}). No refund is made.
UNKNOWNAny other internal CDR state. Response-only, not accepted as a filter.

paymentMethod is one of CREDIT_CARD, WALLET, PAYPAL, CORPORATE, PREPAID_WALLET, CHECKOUT, UNKNOWN. billingProfile is PRIVATE, COMPANY (business profile without organization) or ORGANIZATION (business profile with organization).

Settlement processing status

processingStatus values of the async settlement record:

processingStatusMeaning
RECEIVEDWaiting for processing or for a booked retry.
PAYMENT_IN_FLIGHTAn external payment was requested and its result is not known yet.
COMPLETEDSettlement finished, the debt is covered.
FAILEDCollection failed. Consult overview.actions.
QUARANTINEDAutomation stopped. Operator action needed (see overview.diagnostics.quarantineReason).
NO_PAYMENT_REQUIREDNothing to collect.
OPERATOR_EXEMPTEDOperator exempted the debt. No refund is made.
stateDiagram-v2
    [*] --> RECEIVED: CDR received
    RECEIVED --> NO_PAYMENT_REQUIRED: nothing to collect
    RECEIVED --> PAYMENT_IN_FLIGHT: leg attempt sent
    PAYMENT_IN_FLIGHT --> COMPLETED: remaining amount covered
    PAYMENT_IN_FLIGHT --> RECEIVED: shortfall, automatic retry booked
    PAYMENT_IN_FLIGHT --> FAILED: no further attempt possible
    PAYMENT_IN_FLIGHT --> QUARANTINED: outcome unsafe to automate
    FAILED --> RECEIVED: RESCHEDULE / REFRESH_PLAN
    FAILED --> OPERATOR_EXEMPTED: exempt
    QUARANTINED --> RECEIVED: RESUME / REFRESH_PLAN (repairable reasons)
    QUARANTINED --> PAYMENT_IN_FLIGHT: RETRY_TRUTH_LOOKUP
    QUARANTINED --> FAILED: ABANDON

Leg statuses: PLANNED, IN_PROGRESS, SUCCEEDED, FAILED, SOURCE_UNAVAILABLE, SOURCE_EXHAUSTED, SKIPPED, CANCELLED, UNKNOWN.
Attempt states: SCHEDULED, IN_PROGRESS, SUCCEEDED, FAILED_FINAL, FAILED_RETRYABLE, RECONCILIATION_REQUIRED (outcome unknown, not a decline), UNKNOWN.

Reading rules for settlement details:

  • Use overview.summary (payableAmount, confirmedCollectedAmount, remainingAmount, coverageExcess) for money. Do not sum attempt.amount: it is the requested amount, not collected money.
  • legs show only the current snapshot. overview.summary.contributions aggregate confirmed money across all snapshots, including contributions kept after REFRESH_PLAN.
  • amountsAvailable: false means amounts cannot be interpreted reliably; amount fields are then null, never 0.
  • A positive coverageExcess records over-collection. It does not mean a refund was made.
  • Use overview.actions[].enabled to decide which recovery mode to offer. requiresExecutionCheck: true means the POST re-checks payment sources and can still refuse.
  • overview.retry.nextAutomaticRetryAt is the next booked automatic retry (UTC), null when none is booked.
  • 404 with no body means no async settlement record exists. The CDR can still exist.
  • The GET never contacts payment providers and never changes state.

Typical flow: investigate and recover a failed payment

sequenceDiagram
    autonumber
    participant C as Your backend
    participant API as Public API

    C->>API: GET /backoffice/payments/config
    API-->>C: { configuredEngine, supportedRecoveryModes, ... }
    C->>API: GET /backoffice/payments?status=FAILED&page=1&size=20
    API-->>C: page of payments (paymentStatus FAILED)
    C->>API: GET /backoffice/payments/cdrs/{cdrId}/settlement-details
    API-->>C: legs, overview.summary, overview.actions
    alt an action is enabled
        C->>API: POST /backoffice/payments/cdrs/{cdrId}/reschedule-settlement?mode=RESCHEDULE
        API-->>C: 202 { cdrId, outcome: SCHEDULED }
        C->>API: GET /backoffice/payments/cdrs/{cdrId}/settlement-details
    else debt must be written off
        C->>API: POST /backoffice/payments/exempt/{cdrId}
        API-->>C: 204
    end

List failed payments:

curl -s -H "Authorization: $API_KEY" \
  "https://<host>/backoffice/payments?status=FAILED&fromDate=2026-10-01T00:00:00&toDate=2026-11-01T00:00:00&page=1&size=20"
{
  "content": [
    {
      "cdrId": "cdr-123",
      "operatorName": "Example CPO",
      "evseId": "PL*EXA*E0001*1",
      "userId": "user-42",
      "totalCost": 50.00,
      "currency": "PLN",
      "totalEnergy": 21.5,
      "paymentStatus": "FAILED",
      "paymentMethod": "CREDIT_CARD",
      "startedAt": "2026-10-20T10:00:00Z",
      "stoppedAt": "2026-10-20T10:55:00Z",
      "cdrTokenType": "APP_USER",
      "billingProfile": "PRIVATE",
      "freeOfCharge": false
    }
  ],
  "totalElements": 1,
  "totalPages": 1,
  "currentPage": 1
}

Read settlement details (excerpt):

curl -s -H "Authorization: $API_KEY" \
  "https://<host>/backoffice/payments/cdrs/cdr-123/settlement-details"
{
  "cdrId": "cdr-123",
  "snapshotVersion": 2,
  "processingStatus": "FAILED",
  "currency": "PLN",
  "strategy": { "name": "flexible-multi-leg", "version": 1, "entryContext": "OFF_SESSION" },
  "overview": {
    "summary": {
      "amountsAvailable": true,
      "payableAmount": 50.00,
      "confirmedCollectedAmount": 20.00,
      "remainingAmount": 30.00,
      "coverageExcess": 0.00,
      "currency": "PLN"
    },
    "actions": [
      { "mode": "RESCHEDULE", "enabled": true, "reasonCode": null, "requiresExecutionCheck": true },
      { "mode": "ABANDON", "enabled": false, "reasonCode": "NOT_RETRYABLE", "requiresExecutionCheck": false }
    ],
    "retry": { "enabled": false, "automaticRetryCount": 0, "nextAutomaticRetryAt": null },
    "diagnostics": { "hasUnansweredCustomerFundsOperation": false, "quarantineReason": null }
  }
}

Recovery operations

Four operations can move a payment forward. They are not interchangeable.

OperationEndpointAllowed whenEffect
ReprocessPOST /backoffice/payments/reprocessCDR is RECEIVED, PAYMENT_DEFERRED, PAYMENT_SKIPPED or PAYMENT_FAILED internally, and comes from an OCPI CPOAsks the CPO for its CDRs again (OCPI reconciliation over a time window around the CDR's last CPO update). A re-received CDR resumes its existing settlement; collected money is not charged again.
Recover settlementPOST /backoffice/payments/cdrs/{cdrId}/reschedule-settlement?mode=...Async engine owns the settlement and the mode is enabled in overview.actionsActs on the async settlement only. Does not run CDR reconciliation.
ExemptPOST /backoffice/payments/exempt/{cdrId}paymentStatus is FAILED and, if an async settlement exists, it accepts the exemptionSets the CDR to EXEMPTED, releases pending preauthorizations / wallet holds. Collected money stays collected, nothing is refunded.
Plan continuation/backoffice/payments/settlement-plan-continuationsHistorical async settlements, activation enabled by deployment flagOperations repair for historical plans. See below.

Reprocess

curl -s -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" \
  -d '{ "cdrIds": ["cdr-123", "cdr-456"] }' \
  "https://<host>/backoffice/payments/reprocess"

Returns 204 immediately; the work runs in the background in batches. Only one reprocess run is allowed at a time ("A operation was already triggered. Please wait for the current process to finish"). CDRs in other states, or whose connector is not an OCPI connector, are skipped.

Recovery modes

mode is required. Money already collected always stays collected, and no mode refunds money.

modeWhat it doesTypical use
RESCHEDULERetries now on the plan the settlement already has, only for the remaining amount. Reopens every failed payment source, including hard declines (automatic retry does not). Does not consume or reset the automatic retry budget.FAILED or RECEIVED settlement, customer fixed their card. Blocked for quarantined settlements.
REFRESH_PLANRe-plans the remaining amount with the strategy configured today, starting a new snapshot. Keeps earlier contributions.Strategy configuration changed and should apply. Refused while a payment is in flight or when nothing remains.
RETRY_TRUTH_LOOKUPAsks the provider once more about a payment it never answered for. Requests no new payment.Quarantined with an unanswered payment.
RESUMEReleases a quarantined settlement on its existing plan after its input data was fixed. Keeps attempts already used.Quarantine caused by missing/unusable data. Refused while a payment is unanswered.
ABANDONCloses the settlement for good. Nothing more is attempted.Quarantined settlement whose money is accounted for but cannot be settled automatically. Refused while a payment is unanswered.
curl -s -X POST -H "Authorization: $API_KEY" \
  "https://<host>/backoffice/payments/cdrs/cdr-123/reschedule-settlement?mode=RESCHEDULE"
{ "cdrId": "cdr-123", "outcome": "SCHEDULED" }

The HTTP status follows the outcome:

HTTPoutcome
200COMPLETED
202SCHEDULED, TRUTH_RECORDED, ABANDONED, OPERATOR_EXEMPTED, WAITING_FOR_CUSTOMER_FUNDS_TRUTH, RETRYABILITY_UNKNOWN_ASK_AGAIN, BLOCKED_CUSTOMER_FUNDS_UNANSWERED
404NOT_FOUND (no async settlement for this CDR)
409NOT_RETRYABLE, BLOCKED_QUARANTINED
501ASYNC_DISABLED (async engine not active)

RETRYABILITY_UNKNOWN_ASK_AGAIN means no payment source could be reached to decide; try again later. Disabled actions in settlement details carry a reasonCode: ASYNC_DISABLED, MANUAL_RETRY_BLOCKED, STATE_NOT_RETRYABLE, NOT_RETRYABLE, CUSTOMER_FUNDS_UNANSWERED, BLOCKED_CUSTOMER_FUNDS_UNANSWERED, MONEY_DATA_UNAVAILABLE, NO_REMAINING_AMOUNT, BLOCKED_QUARANTINED.

Automatic retry

When enabled (automaticRetryEnabled in /backoffice/payments/config), the async engine books later passes over the same frozen plan for the remaining amount. The delays are deployment configuration (default 6h, 24h, 24h, so three retries after the first pass). Automatic retry does not retry a hard-declined source. A quarantined settlement never resumes by itself.

Exempt

curl -s -X POST -H "Authorization: $API_KEY" \
  "https://<host>/backoffice/payments/exempt/cdr-123"

204 on success. 404 when the CDR does not exist or is not in payment-failed state. 409 when the async settlement refuses the exemption in its current state.

Settlement plan continuation

Operations tool for async settlements created with an older plan interpretation. It continues only the outstanding debt and keeps captures, decline history and retry counters. It does not select today's strategy (unlike REFRESH_PLAN), and does not undo exemptions.

  1. GET /backoffice/payments/settlement-plan-continuations?afterCdrId=&limit=25 — list assessments, keyset-paged by CDR id (limit 1–50, default 25). Pass the last cdrId as afterCdrId for the next page.
  2. GET /backoffice/payments/settlement-plan-continuations/{cdrId} — dry run. Returns snapshotVersion, strategyConfigHash, recipeDigest, remaining, runtimeLegs, historicalContracts and a decision.
  3. POST /backoffice/payments/settlement-plan-continuations/{cdrId} with { "repairId": "<new UUID>", "expectedSnapshotVersion", "expectedConfigHash", "expectedRecipeDigest" } copied from the dry run.

decision values: ELIGIBLE, ACTIVATED, ALREADY_ACTIVATED, DISABLED, NOT_FOUND, STATE_EXCLUDED, RECONCILIATION_REQUIRED, NO_REMAINING_DEBT, NO_ADAPTABLE_LEGS, MANUAL_REVIEW_REQUIRED, EXPECTATION_MISMATCH, REPAIR_ID_CONFLICT. Activation requires planContinuationEnabled: true in /backoffice/payments/config; discovery and dry runs are always available. A repeated accepted command is a no-op.

Settlement strategies

A settlement strategy is a named, versioned policy that decides which payment sources may cover a CDR and in which order. It is deployment configuration; the API only reads it.

  • A strategy has variants. One variant is selected per CDR by entry context (NO_PAYMENT, OFF_SESSION, PROVIDER_FUNDS_HOLD, PREPAID_WALLET), then by satisfied entry preconditions (for example off-session-candidate-available, provider-funds-hold-available, prepaid-wallet-available), then by lowest priority.
  • A variant is an ordered list of blocks (for example full-off-session, funds-hold-capture, remainder-off-session, wallet-free-balance-debit, wallet-overdraft-debit, cleanup of an unused preauthorization). A block can be required or optional and can activate immediately, after named predecessor outcomes, or only after the debt is paid (cleanup).
  • Coverage requirement says whether a block must settle the whole remainder (must_settle_remaining), may contribute part of it (may_contribute_partial), or moves no money (no_customer_funds). Requested amounts are always capped at the remaining amount.
  • The plan selected for a CDR is frozen: automatic retry and RESCHEDULE reuse it; only REFRESH_PLAN adopts the current strategy. The frozen strategy is visible in settlement details as strategy.name, version, configHash, selectedVariantName, entryContext.
  • Prepaid-wallet balance and overdraft limits are enforced by the wallet at debit time, not by the strategy.
curl -s -H "Authorization: $API_KEY" \
  "https://<host>/backoffice/payments/settlement-strategies"
{
  "active": "example-flexible-multi-leg",
  "strategies": [
    {
      "name": "example-flexible-multi-leg",
      "version": 1,
      "configHash": "3f9a...",
      "canonicalForm": "...",
      "source": "...",
      "active": true
    }
  ]
}

VAT and commission

VAT rates are stored per country (ISO 3166 alpha-2) as a percentage 0–100. PUT upserts.

curl -s -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" \
  -d '{ "countryCode": "PL", "rate": 23 }' \
  "https://<host>/backoffice/payments/vat"

GET /backoffice/payments/vat/{countryCode} returns { "countryCode": "PL", "rate": 23 } or 404. GET /backoffice/payments/vat returns a page sorted by country ascending.

The payments commission is a single percentage (0–100): GET /backoffice/payments/commission returns { "percentage": 5.00 }, POST /backoffice/payments/commission with { "percentage": 5.00 } changes it (204).

External reference

PUT /backoffice/payments/{cdrId}/external-reference with { "externalInvoiceNumber": "INV-2026-0001" } (max 255 chars) stores an invoice number from an external billing system on the CDR (204). It is returned as the external invoice number in payment details.

Invoices

POST /backoffice/invoices generates an invoice PDF for a CDR. Available only when the invoices feature is enabled in the deployment. Accept-Language (optional) selects the invoice language.

curl -s -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" \
  -H "Accept-Language: en" \
  -d '{ "cdrId": "cdr-123" }' \
  "https://<host>/backoffice/invoices"
{ "status": "DONE", "invoicePdf": "https://files.example.com/invoices/cdr-123.pdf" }

When status is not DONE, invoicePdf may be missing. 400 is returned when the CDR state does not allow invoice generation, 404 when the CDR or invoice template is not found.

Charging pool payment methods

PUT /backoffice/charging-pools/{chargingPoolId}/payment-methods controls payment at a private charging pool:

  • { "paymentMethods": ["FREE_OF_CHARGE"] } switches payments off. Sessions there are free for users with access, except corporate sessions, which are always settled to their organization. Their CDRs report freeOfCharge: true and paymentStatus: PAYMENT_SKIPPED.
  • { "paymentMethods": null } removes the assignment. The pool offers the environment's payment methods and is paid like a public pool.

No other list is supported. Returns 204.

Listing parameters

GET /backoffice/payments:

ParameterNotes
page1-based, default 1, min 1
sizedefault 10, min 1
cdrId, userIdexact match
userEmail, evseIdcase-insensitive substring
fromDate, toDateyyyy-MM-ddTHH:mm:ss, no offset; filter on session start, fromDate inclusive, toDate exclusive
statusPENDING, PAID, PAYMENT_SKIPPED, FAILED, EXEMPTED
cdrTokenTyperepeatable: APP_USER, RFID, OTHER, AD_HOC_USER
billingProfilerepeatable: PRIVATE, COMPANY, ORGANIZATION

Sorting is fixed: newest session start first. GET /backoffice/payments/filters returns the values to show in filter UIs (cdrTokenTypes without AD_HOC_USER, profiles).

GET /backoffice/payments/vat: page 1-based (default 1), size default 10, max 100, sorted by country.

Related


Did this page help you?