Skip to main content

Payment Flows

How payments move through your integration: the lifecycle, capabilities, reconciliation, and — most importantly — asynchronous (3-D Secure / redirect) flows and the result callback.

Read first: Overview & Concepts

The cross-cutting rules every integration follows — the five outcomes (SUCCESS/PENDING/DECLINED/RETRIABLE/UNKNOWN), the response contract (HTTP 200 + status; how LoyaltyPlant interprets your HTTP codes), idempotency, and the error-code catalogue (PaymentErrorCode) — live in the Payments API specification and the SDK guide (which includes the full SDK reference inline), not here. This page covers the flows only.


1. The payment lifecycle

Your integration supports one of two shapes, declared via capabilities.

1.1 Two-phase (CAPTURE)

Authorize first (hold), capture later (settle), or reverse (release).

  • capture receives the transactionReference you returned from authorize. amount is optional — omit to capture the full amount, or pass a smaller value for a partial capture.
  • reverse voids an authorization that was never captured.
  • refund returns funds after capture (supports partial amounts).

1.2 Single-phase (SALE)

One call authorizes and captures.

Refund later with POST /v1/refund (if you declare DIRECT_REFUND).

Declare CAPTURE or SALE (you may declare both). LoyaltyPlant chooses authorize for two-phase and sale for single-phase based on what you declare.

2. Capabilities & dispatch

GET /v1/capabilities returns the features you support. LoyaltyPlant caches the list and only calls operations whose capability you declared.

CapabilityUnlocksNotes
CAPTUREPOST /v1/authorize + POST /v1/captureTwo-phase
SALEPOST /v1/saleSingle-phase
DIRECT_REFUNDPOST /v1/refundRefund by transactionReference
REDIRECT_FLOWPENDING + redirectUrl from authorize/saleEnables 3DS/redirect (see §4)
DIRECT_WEBHOOKThe result callbackYour PSP webhooks you; you call LP back
STATUS_POLLINGPOST /v1/inquire-statusReconciliation (see §3)
TOKENIZATIONCard-on-file / one-clickNo dedicated endpoint — handled inside authorize/sale using settings
APPLE_PAY / GOOGLE_PAYWallet paymentsNo dedicated endpoint — handled inside authorize/sale

Capabilities without their own endpoint. TOKENIZATION, APPLE_PAY, GOOGLE_PAY don't add new methods. You "support" them by handling the relevant data inside your authorize/sale implementation (the payment instrument and any tokens arrive via the request and the provisioned settings). Declaring them tells LoyaltyPlant you can accept those payment types.

reverse has no gating capability — implement it whenever you support CAPTURE.

3. Reconciliation: status inquiry

When an outcome is ambiguous — a timeout, an UNKNOWN, or a PENDING whose callback never arrived — LoyaltyPlant calls POST /v1/inquire-status (if you declare STATUS_POLLING) to ask your PSP for the truth.

Return the PSP's current status:

{ "status": "CAPTURED", "transactionReference": "psp_txn_7HagaIn2", "rawStatus": "captured" }

status is one of AUTHORIZED, CAPTURED, VOIDED, REFUNDED, DECLINED, EXPIRED, PENDING, NOT_FOUND. This is read-only and carries no Idempotency-Key. Implementing status polling makes your integration far more robust against lost callbacks and network blips — it is strongly recommended for any integration that uses async flows.


4. Asynchronous flows (3-D Secure / redirect) — the keystone

Some payments can't finish in the single HTTP call from LoyaltyPlant: the customer must complete 3-D Secure or be redirected to a bank/hosted page. This is the part integrations most often get wrong, so read it carefully.

4.1 The shape of an async payment

  1. LoyaltyPlant calls POST /v1/authorize (or /v1/sale).

  2. Your PSP says "authentication required" and gives you a redirect URL.

  3. You return 200 PENDING with that redirectUrl and an optional expectedTtlSeconds:

    {
    "status": "PENDING",
    "transactionReference": "psp_txn_7HagaIn2",
    "redirectUrl": "https://acs.issuer-bank.example/3ds/challenge?token=…",
    "expectedTtlSeconds": 300
    }
    • transactionReference is mandatory here — LoyaltyPlant stores it and later matches your callback to this suspended payment by this value.
    • expectedTtlSeconds tells LoyaltyPlant how long to wait before treating the payment as timed out (default 15 minutes if omitted).
  4. LoyaltyPlant redirects the customer to redirectUrl. They authenticate.

  5. Your PSP notifies your service (its own webhook to an endpoint you own — LoyaltyPlant is not involved here).

  6. You verify that notification and call LoyaltyPlant back to report the final result (§4.2).

  7. LoyaltyPlant resumes the suspended payment and finishes the flow.

To use this flow, declare REDIRECT_FLOW (and DIRECT_WEBHOOK if your PSP webhooks you).

4.2 The result callback

After the payment resolves at your PSP, you report it to LoyaltyPlant with a single call. This callback is not in the set of endpoints you implement — it's an endpoint LoyaltyPlant exposes that you call.

POST https://<payments-service-host>/gate/integration/{integrationId}/result
Authorization: Bearer <inboundToken>
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Content-Type: application/json

{ "status": "SUCCESS", "transactionReference": "psp_txn_7HagaIn2" }
ItemValue
URLPOST {payments-service-host}/gate/integration/{integrationId}/result — the host and your integrationId come from onboarding
AuthAuthorization: Bearer <inboundToken> — your inbound token (the one LP gave you for calling back). This is not the outbound token you validate on incoming requests. See General Requirements §4.
Idempotency-KeyA UUID. Retries are safe (see below).
BodyAn IntegrationOperationResponse — usually SUCCESS or DECLINED.
transactionReferenceMust match the value you returned in the PENDING response — that's how LoyaltyPlant finds the suspended payment.

Responses you may get back:

CodeMeaning
200 OKAccepted; the suspended payment was resumed (or had already resumed — a safe replay).
400Invalid payload (e.g. bad JSON, missing transactionReference).
401Invalid inbound token.
404Unknown or inactive integrationId.

Retries & idempotency. Send an Idempotency-Key and retry the callback until you get a 200. Replays are safe: LoyaltyPlant matches the payment by transactionReference, so a callback that arrives after the payment already resumed is a no-op. If you never send the callback, the payment eventually times out (per expectedTtlSeconds) and, if you support STATUS_POLLING, LoyaltyPlant may reconcile by asking your PSP directly.

4.3 Hosted payment pages

Because your service is publicly reachable, the redirectUrl you return can point at a page you host — your own hosted payment page, a PSP drop-in/redirect, etc. LoyaltyPlant simply sends the customer there; you own the page and the PSP acknowledgement.

4.4 Common async mistakes

  • ❌ Returning PENDING without a transactionReference → LoyaltyPlant can't match your later callback, and the payment hangs until it times out.
  • ❌ Sending the callback with the outbound token instead of the inbound token → 401.
  • ❌ A transactionReference in the callback that differs from the one in the PENDING response → LoyaltyPlant can't find the payment (logged, no resume).
  • ❌ Not verifying the PSP webhook signature before trusting it → you may report a forged result. Always verify on your side.
  • ❌ Treating a 200 OK from the callback as "charge the customer again" on retry — it's idempotent; just stop retrying.
  • ❌ Declaring REDIRECT_FLOW but never implementing the callback → every 3DS payment stalls.

Next: choose your path — SDK Integration Guide → (Java) or API Integration Guide → (any language).