Payment lifecycle

Moving money on revenexx — create, confirm, capture, cancel and refund, the order-scoped capture a warehouse calls, the dunning scan, and where provider callbacks land.

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

text
                        ┌──────────────┐
   created ──────────►  │requires_action│ ──confirm──┐
      │                 └──────────────┘             │
      │                                              ▼
      ├──────────────────────────────────►  authorized ──capture──► captured ──refund──► refunded
      │                                              │
      └──► failed                                    └──cancel──► cancelled
                                                          ▲
                        created / requires_action / authorized
TransitionStarts fromNote
confirmrequires_action onlyA payment that already came back authorized needs no confirm; the lattice answers 400.
captureauthorized only
cancelcreated, requires_action or authorizedThe only transition with three starting states — everything short of captured can still be released.
refundcaptured onlyMoney 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

Request
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 choosekind, 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:

MethodResult
self_managed (invoice, prepayment)authorized at once, with the dunning clock already started.
pspcaptured, 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.

What is never stored: the 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

Request
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

Request
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:

PolicyCapture happens
immediateIn the same request that created the payment. This route is never reached.
manualHere, when an operator says so.
on_shipThrough 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

Request
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

Request
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

Request
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"}'
All or nothing. There is no partial refund and no second one to express — a refunded payment is refunded in full, and a repeat is a 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:

CodeMeans
provider_unavailableNo driver for this provider in this deployment. See Payment methods.
provider_unreachableThe PSP could not be reached.
provider_not_configuredNo credentials, or the provider is disabled.
provider_declinedThe PSP said no.
provider_errorAnything else the PSP reported.

The dunning scan

Request
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.

It classifies and does not send. A stage change emits payment.updated; what a reminder looks like is your workflow.

Provider callbacks

text
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_code names.
  • Orders — recording the result on the order.
  • Returns — the flow a refund hangs off.
  • Webhooks — how a provider callback reaches the platform.
Was this page helpful?