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:
| Value | Where | Meaning |
|---|---|---|
paymentStatus | GET /backoffice/payments, GET /backoffice/payments/{id} | Business status of the CDR payment. Exists for every CDR. |
processingStatus | GET /backoffice/payments/cdrs/{cdrId}/settlement-details | State 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:
paymentStatus | Meaning |
|---|---|
PENDING | CDR received, payment not finished yet (includes deferred payments). |
PAID | Payment collected. Also returned when payment was skipped for a CDR that is not free of charge. |
PAYMENT_SKIPPED | No payment taken because the charge was free of charge (private pool flagged FREE_OF_CHARGE). |
FAILED | Payment failed. The CDR can be exempted or recovered. |
EXEMPTED | An operator wrote the debt off (POST /backoffice/payments/exempt/{cdrId}). No refund is made. |
UNKNOWN | Any 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:
processingStatus | Meaning |
|---|---|
RECEIVED | Waiting for processing or for a booked retry. |
PAYMENT_IN_FLIGHT | An external payment was requested and its result is not known yet. |
COMPLETED | Settlement finished, the debt is covered. |
FAILED | Collection failed. Consult overview.actions. |
QUARANTINED | Automation stopped. Operator action needed (see overview.diagnostics.quarantineReason). |
NO_PAYMENT_REQUIRED | Nothing to collect. |
OPERATOR_EXEMPTED | Operator 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 sumattempt.amount: it is the requested amount, not collected money. legsshow only the current snapshot.overview.summary.contributionsaggregate confirmed money across all snapshots, including contributions kept afterREFRESH_PLAN.amountsAvailable: falsemeans amounts cannot be interpreted reliably; amount fields are thennull, never0.- A positive
coverageExcessrecords over-collection. It does not mean a refund was made. - Use
overview.actions[].enabledto decide which recovery mode to offer.requiresExecutionCheck: truemeans thePOSTre-checks payment sources and can still refuse. overview.retry.nextAutomaticRetryAtis the next booked automatic retry (UTC),nullwhen none is booked.404with no body means no async settlement record exists. The CDR can still exist.- The
GETnever 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.
| Operation | Endpoint | Allowed when | Effect |
|---|---|---|---|
| Reprocess | POST /backoffice/payments/reprocess | CDR is RECEIVED, PAYMENT_DEFERRED, PAYMENT_SKIPPED or PAYMENT_FAILED internally, and comes from an OCPI CPO | Asks 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 settlement | POST /backoffice/payments/cdrs/{cdrId}/reschedule-settlement?mode=... | Async engine owns the settlement and the mode is enabled in overview.actions | Acts on the async settlement only. Does not run CDR reconciliation. |
| Exempt | POST /backoffice/payments/exempt/{cdrId} | paymentStatus is FAILED and, if an async settlement exists, it accepts the exemption | Sets the CDR to EXEMPTED, releases pending preauthorizations / wallet holds. Collected money stays collected, nothing is refunded. |
| Plan continuation | /backoffice/payments/settlement-plan-continuations | Historical async settlements, activation enabled by deployment flag | Operations 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.
mode | What it does | Typical use |
|---|---|---|
RESCHEDULE | Retries 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_PLAN | Re-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_LOOKUP | Asks the provider once more about a payment it never answered for. Requests no new payment. | Quarantined with an unanswered payment. |
RESUME | Releases 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. |
ABANDON | Closes 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:
| HTTP | outcome |
|---|---|
200 | COMPLETED |
202 | SCHEDULED, TRUTH_RECORDED, ABANDONED, OPERATOR_EXEMPTED, WAITING_FOR_CUSTOMER_FUNDS_TRUTH, RETRYABILITY_UNKNOWN_ASK_AGAIN, BLOCKED_CUSTOMER_FUNDS_UNANSWERED |
404 | NOT_FOUND (no async settlement for this CDR) |
409 | NOT_RETRYABLE, BLOCKED_QUARANTINED |
501 | ASYNC_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.
GET /backoffice/payments/settlement-plan-continuations?afterCdrId=&limit=25— list assessments, keyset-paged by CDR id (limit1–50, default 25). Pass the lastcdrIdasafterCdrIdfor the next page.GET /backoffice/payments/settlement-plan-continuations/{cdrId}— dry run. ReturnssnapshotVersion,strategyConfigHash,recipeDigest,remaining,runtimeLegs,historicalContractsand adecision.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 exampleoff-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
RESCHEDULEreuse it; onlyREFRESH_PLANadopts the current strategy. The frozen strategy is visible in settlement details asstrategy.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 reportfreeOfCharge: trueandpaymentStatus: 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:
| Parameter | Notes |
|---|---|
page | 1-based, default 1, min 1 |
size | default 10, min 1 |
cdrId, userId | exact match |
userEmail, evseId | case-insensitive substring |
fromDate, toDate | yyyy-MM-ddTHH:mm:ss, no offset; filter on session start, fromDate inclusive, toDate exclusive |
status | PENDING, PAID, PAYMENT_SKIPPED, FAILED, EXEMPTED |
cdrTokenType | repeatable: APP_USER, RFID, OTHER, AD_HOC_USER |
billingProfile | repeatable: 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
Updated 14 days ago