Payments

Payment lifecycle

Every VenPays payment is identified by a track_id and moves through engine states before merchants see a simplified status.


Happy path

Your backend initiates payment
        ↓
Payment Engine creates track_id (+ payment_url or native session)
        ↓
Customer authenticates (hosted page / Apple Pay / Benefit)
        ↓
PSP result returns to Payment Engine
        ↓
Transaction status updated
        ↓
Webhook (PAYMENT_SUCCESS or PAYMENT_FAILED) → your server
        ↓
You confirm with payment-status and fulfill the order

Merchant-facing statuses

From POST /merchant/payment-status:

StatusMeaning
pending_merchantAwaiting next merchant/engine step
pending_userCustomer on checkout
redirectingMid-redirect / callback
successPayment completed
failedFailed or declined
errorSystem / initiation error
cancelledShopper left checkout, or the payment was not completed within 30 minutes

These values match the console. Full reference: Payment Status API — Detailed Transaction States.

Engine statuses are the same set used during checkout. See Payment statuses.


Timeouts and pending

A network timeout on your HTTP client does not mean the payment failed. The customer may still complete checkout. Always:

  1. Persist track_id before relying on the response
  2. Poll or await webhooks
  3. Use authenticated payment-status for reconciliation

Native Apple Pay authorize may return HTTP 202 with processing under concurrency — recover with the SDK status GET while the native session is valid.


Unpaid checkout

Card checkout, Apple Pay, Benefit, and Benefit Web Checkout stay open while status is pending_merchant, pending_user, or redirecting. If the customer does not complete payment within 30 minutes, VenPays sets the status to cancelled.

No extra field is required on the payment request. You can still cancel sooner when the shopper leaves checkout. After 30 minutes, POST /merchant/payment-status returns cancelled even if cancel was never called.

A PAYMENT_FAILED webhook is sent for that track_id. Confirm with payment-status before you fulfill: the status is cancelled. To charge again, create a new payment.

The native Apple Pay session token still expires after about 15 minutes. That token expiry is separate from this 30-minute cancel.


Session and authorization

status is the money outcome. Session and authorization say where the attempt is, which matters most for native Apple Pay: a session can exist before anyone pays, and an authorize request can reach VenPays while the money status is still in flight.

Ask for both on the authenticated status call:

{
  "track_id": "11111111-2222-3333-4444-555555555555",
  "include_lifecycle": true
}

Omit the flag and the response stays { "status", "track_id" }. With the flag, the response adds lifecycle. Field reference and a full example: Payment Status.

Session

session.stateMeaning
activeSession issued, not used yet
consumedThe customer started authorization
expiredThe session passed expires_at
cancelledThe session was closed

created_at, expires_at, and consumed_at are UTC timestamps. consumed_at is set the first time the session is used.

Native Apple Pay initiation also returns expires_at on the session itself (about 15 minutes). After that, the app must start a new payment. Polling status with include_lifecycle marks an unused, past-expiry session as expired. That does not by itself mean the customer was charged.

Authorization

authorization.stateMeaning
noneAuthorize has not reached VenPays
receivedThe authorize request is recorded
processingThe processor call is in progress
completedThe processor result is recorded; use status for success or failure

authorization.received is true once VenPays has the request. authorization.request_id is the correlation id for that attempt. Quote it with track_id if you contact support.

status can stay pending_merchant while authorization.state is processing. Do not read a processing authorization as a failed or abandoned payment.

Signals

lifecycle.client_signals is derived for you. Do not store a parallel copy as the source of truth; read it from the status call.

SignalMeaning
still_processingThe attempt can still finish. Do not fulfill and do not start a replacement charge.
authorization_receivedVenPays has the authorize request
session_usedThe session was consumed
session_expired_unusedThe session expired and authorize never arrived
waiting_foruser_action, processor, or merchant_action
safe_to_abandonYou can stop waiting on this attempt

Native Apple Pay writes the session and authorization fields. Card, Benefit, and other methods still return in-flight signals from status, so an open checkout is not reported as abandoned.


Safe retry

lifecycle.reattempt is the rule for starting another charge for the same attempt.

decisionWhat to do
retry_allowedStart a new payment. This attempt did not capture.
do_not_retryDo not start another charge. It already succeeded, or it may still complete.
reconcile_firstRead status again, and your webhook, before any new payment. The authorize request may have reached VenPays.

allow_retry is true only for retry_allowed. reconcile_required is true only for reconcile_first. terminal_kind and reason say why.

SituationDecision
success, refunded, or partial_refunddo_not_retry — already charged
failed or a processor declineretry_allowed — new payment
pending_merchant, pending_user, or redirecting while the session is still livedo_not_retry — it may still complete
Shopper cancelled before any authorizationretry_allowed
Unused session expired, or the 30-minute unpaid cancelretry_allowed
In-flight request cancelled after authorization may have been sentreconcile_first
You cannot tell whether authorize was sentreconcile_first

On iOS, the same split is userCancelledBeforeAuthorization (safe to retry immediately) and networkRequestCancelled (reconcile by track_id first). See Apple Pay iOS SDK.

Browser and Flutter checkout cancel with POST /v1/sdk/checkout/{track_id}/cancel. The call is idempotent: success and failed are left unchanged. See JavaScript checkout and Flutter checkout.

Fulfill only on success together with your webhook. A retry is a new initiation. Do not send a second charge against a track_id that is already success.


What to confirm in your integration

  • [ ] Status calls without include_lifecycle still read status and track_id only
  • [ ] With include_lifecycle, you can tell an unused session from one that received authorization
  • [ ] You do not fulfill while still_processing is true
  • [ ] You start a new payment only when reattempt.decision is retry_allowed
  • [ ] Apple Pay handles userCancelledBeforeAuthorization and networkRequestCancelled separately
  • [ ] Checkout abandon calls cancel, and a success payment is not overwritten
  • [ ] Hosted card checkout shows your merchant name, logo, and website (Cards)