Payment lifecycle
A payment is one ledger row with one amount and one status. That single fact explains most of the API: there is no partial capture, no partial refund and no per-line payment, because there is nothing to express them on.
Base path: https://api.revenexx.com/v1/payments.
The status lattice
┌──────────────┐
created ──────────► │requires_action│ ──confirm──┐
│ └──────────────┘ │
│ ▼
├──────────────────────────────────► authorized ──capture──► captured ──refund──► refunded
│ │
└──► failed └──cancel──► cancelled
▲
created / requires_action / authorized
| Transition | Starts from | Note |
|---|---|---|
confirm | requires_action only | A payment that already came back authorized needs no confirm; the lattice answers 400. |
capture | authorized only | |
cancel | created, requires_action or authorized | The only transition with three starting states — everything short of captured can still be released. |
refund | captured only | Money that was only authorized is cancelled, not refunded. |
A captured payment is not cancellable at all, and a refunded one cannot be refunded again — the lattice answers 400 rather than guessing what was meant.
Create
curl -X POST https://api.revenexx.com/v1/payments \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{
"method_code": "invoice",
"amount": 249.90,
"currency": "EUR",
"order_ref": "ORD-000123",
"contact_id": "…",
"idempotency_key": "checkout-9f3a…"
}'
The checkout's write: it opens the ledger row and takes it as far as the named method allows, in one call.
method_code and amount are required. Nothing else about the money is yours to choose — kind, provider and fee_amount are read off the method that method_code names, so a caller can neither pick an acquirer nor discount its own fee. amount: 0 is legal (free orders); negative is 400.
Eligibility is enforced here and not only in the checkout UI: the same country and order-value rules answer 422 if the method does not apply to this buyer.
What comes back depends on the method:
| Method | Result |
|---|---|
self_managed (invoice, prepayment) | authorized at once, with the dunning clock already started. |
psp | captured, or authorized, or requires_action with next_action — the instruction the storefront must carry out, typically a redirect. next_action is set at that status and at no other. |
Send an idempotency_key. A repeat of the same call answers 200 with the payment that key already named, unchanged and not re-authorized.
instrument, token or card you send is handed to the driver in-process. No token and no card number is written to the row.Confirm — the other half of a redirect
curl -X POST "https://api.revenexx.com/v1/payments/{id}/confirm" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
The create answered requires_action, the storefront carried out the next_action — a 3-D Secure step, a wallet approval, a bank login — and this asks the PSP how it went and writes the answer to the ledger. next_action is cleared whatever the outcome.
Where the tenant's auto_capture_policy is immediate, the money is taken straight after authorization in the same request, so a successful confirm can come back captured rather than authorized. A failed auto-capture does not fail the confirm — a good authorization is worth more than a tidy status.
Capture
curl -X POST "https://api.revenexx.com/v1/payments/{id}/capture" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
Collects money that is currently only reserved. There is no request body and no amount — a capture is the whole authorization or nothing.
Which route you use depends on auto_capture_policy:
| Policy | Capture happens |
|---|---|
immediate | In the same request that created the payment. This route is never reached. |
manual | Here, when an operator says so. |
on_ship | Through the order-scoped route below. |
On a self_managed payment it takes no PSP anywhere near it: it records that an invoice or prepayment was paid, and stops the dunning clock.
Refused with 422 once the authorization is older than the tenant's capture_expiry_days — the message carries both numbers. An expired authorization is declined by the provider anyway, and a 422 here is the cheap version of finding out later.
Capture by order reference
curl -X POST "https://api.revenexx.com/v1/payments/orders/ORD-000123/capture" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
The hook auto_capture_policy: "on_ship" was written for: fulfilment knows the order it shipped, not the payment ids behind it. The shipment calls this with the reference it already holds and the money for that order is collected in one request.
It captures every authorized payment on that reference and reports the rest instead of failing — an order whose payment was already captured is a successful no-op, and a provider that refuses one payment lands in skipped.
payments.order_ref is nullable and has no foreign key. This route is exactly as good as the reference the checkout wrote onto the payment.Cancel
curl -X POST "https://api.revenexx.com/v1/payments/{id}/cancel" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"reason":"Buyer abandoned the checkout"}'
Drops the claim before any money has been taken — the abandoned basket, the buyer who never came back from the redirect, the invoice an operator writes off.
Unlike capture and refund it has no time window. capture_expiry_days and refund_window_days do not apply, so a stale authorization can always be released even once it is too late to collect.
On a PSP payment the provider is called and reason is passed to it, so it reaches the PSP's own cancellation-reason field as well as being stored under metadata.cancel_reason. Cancelling stops the dunning clock: the stage goes back to none and the due date is cleared.
Refund
curl -X POST "https://api.revenexx.com/v1/payments/{id}/refund" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"reason":"Return completed, goods received"}'
400 because refunded is not a status a refund starts from.reason is handed to the driver in the same call, so it reaches the PSP's own refund-reason field rather than being a note only this database sees, and is stored under metadata.refund_reason.
On a self_managed payment nothing is sent anywhere: it records that the merchant paid the buyer back by their own means.
Refused with 422 once the capture is older than the tenant's refund_window_days — past that the provider stops accepting a refund against the transaction and it has to be made by bank transfer.
Errors are classified
A failed payment carries an error_code from a closed set, so a caller can branch:
| Code | Means |
|---|---|
provider_unavailable | No driver for this provider in this deployment. See Payment methods. |
provider_unreachable | The PSP could not be reached. |
provider_not_configured | No credentials, or the provider is disabled. |
provider_declined | The PSP said no. |
provider_error | Anything else the PSP reported. |
The dunning scan
curl -X POST https://api.revenexx.com/v1/payments/dunning/scan \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
Classifies every unpaid self-managed payment as on time, reminder due or overdue, from payment_reminder_after_days and overdue_after_days. It writes dunning_stage (none, reminder, overdue) and dunning_due_at, and reports PSP payments still waiting on a callback longer than webhook_stale_after_minutes.
A pure function of each payment's age, so it is idempotent. It also runs daily on a schedule.
payment.updated; what a reminder looks like is your workflow.Provider callbacks
POST /v1/payments/webhooks/{provider}
A PSP never posts here directly. It posts to the platform's webhook ingress, which verifies and captures the delivery and dispatches its envelope to this route.
Everything a callback does to the ledger happens here and nowhere else, which makes this the one override point for PSP callback handling: a deployment that needs a provider's callbacks normalised differently replaces this operation instead of touching the lifecycle routes.
Facts only move forward — provider retries and redeliveries are idempotent no-ops, and unverified envelopes are refused. The payment is resolved by psp_payment_id or, failing that, by order_ref.
See Webhooks for the ingress side.
Where to go next
- Payment methods — what a payment's
method_codenames. - Orders — recording the result on the order.
- Returns — the flow a refund hangs off.
- Webhooks — how a provider callback reaches the platform.