Skip to main content

Payment Flows

This guide covers all payment flows supported by 4pay.online: direct card payments, hosted payments, payouts, refunds, and cascade routing.

Transaction Lifecycle​

Every transaction follows this state machine:

created → new → authorizing → charging → charged ✅
│ → failed ❌
└► action_required → rejected ❌

charged → reversed (a settled transaction was returned)
StatusFinalDescription
creatednoTransaction initialized
newnoAccepted, routing and validation in progress
authorizingnoBeing authorised by the provider
action_requirednoWaiting for the payer (3-D Secure, OTP, signature)
chargingnoBeing processed by the provider
chargedyesPayment confirmed, funds received
failedyesProvider error, or cancelled before settlement
rejectedyesValidation, routing, or compliance check failed
reversedyesA settled transaction was returned
refundednoRefund completed — finality is rail-dependent

Act on final statuses only, and treat any status you do not recognise as non-final: the list is not closed, and rails carry their own intermediate states (cancelled, payment_suspended and others). The complete model, including what each final outcome means for the money, is in the protocol specification.

Direct Card Payment​

For PCI DSS certified partners who handle card data directly.

Partner API                     4pay.online                    Provider
│ │ │
├─── POST /transactions ───────►│ │
│ (card data included) │── route + validate ────────►│
│ │ │
│ │◄── 3DS required ───────────┤
│◄── status: action_required ───┤ │
│ │ │
├─── POST /additional_params ──►│── 3DS verification ────────►│
│ (OTP or 3DS data) │ │
│ │◄── charged ────────────────┤
│◄── webhook: charged ──────────┤ │

Create Payment​

A payment needs all five card fields — cardnumber, cardholder, exp_month, exp_year, cvv — and both redirect URLs. A payout needs only cardnumber, and no redirect URLs.

Passing card data through your own server puts you in PCI DSS scope. Unless you hold an attestation, use the hosted flow below.

curl -X POST https://4pay.online/api/v1/transactions \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444" \
-H "Content-Type: application/json" \
-d '{
"params": {
"txid": "order_123",
"type": "payment",
"amount": "10000",
"currency": "USD",
"env": "prod",
"money_storage": {
"type": "bank_card",
"cardnumber": "4111111111111111",
"cardholder": "IVAN PETROV",
"exp_month": "12",
"exp_year": "2027",
"cvv": "123"
},
"description": "Order #123",
"returnUrl": "https://your-site.com/result",
"failUrl": "https://your-site.com/failed"
}
}'

Handle 3DS Authentication​

When the status becomes action_required, the action itself is in auth_params — its type tells you what to do (redirect or auth_3ds_form), and url, method and params tell you where to send the customer. There is no action_required boolean and no auth_url field.

After verification, submit the authentication data:

curl -X POST https://4pay.online/api/v1/transactions/additional_params_for_auth/TX_ID \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444" \
-H "Content-Type: application/json" \
-d '{"params": {"otp": "123456"}}'

Hosted Payment​

For partners without PCI DSS certification. The customer enters card details on a 4pay.online-hosted page.

Partner API                     4pay.online                    Customer
│ │ │
├─── POST /transactions ───────►│ │
│ (NO card data) │ │
│◄── widget_url ────────────────┤ │
│ │ │
│ redirect customer ─────────┼─────────────────────────────►
│ │◄── card data ──────────────┤
│ │── process payment ──► │
│◄── webhook: charged ──────────┤ │

Create Hosted Payment​

curl -X POST https://4pay.online/api/v1/transactions \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444" \
-H "Content-Type: application/json" \
-d '{
"params": {
"txid": "order_456",
"type": "payment",
"amount": "5000",
"currency": "EUR",
"env": "prod",
"hosted": true,
"description": "Order #456",
"returnUrl": "https://your-site.com/result",
"failUrl": "https://your-site.com/failed"
}
}'

Response includes widget_url — redirect the customer there. After payment, the customer returns to returnUrl and you receive a webhook.

Payout​

Send funds from your account to a recipient (card, bank account, wallet).

curl -X POST https://4pay.online/api/v1/transactions \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444" \
-H "Content-Type: application/json" \
-d '{
"params": {
"txid": "payout_789",
"type": "payout",
"amount": "25000",
"currency": "USD",
"env": "prod",
"money_storage": {
"type": "bank_card",
"cardnumber": "4111111111111111"
},
"description": "Withdrawal request #789"
}
}'

Payout follows the same lifecycle: created → new → charging → charged/failed.

Refund​

Issue a full or partial refund on a charged transaction.

Full Refund​

curl -X PUT https://4pay.online/api/v1/transactions/TX_ID \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444" \
-H "Content-Type: application/json" \
-d '{"params": {"type": "refund"}}'

Partial Refund​

curl -X PUT https://4pay.online/api/v1/transactions/TX_ID \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444" \
-H "Content-Type: application/json" \
-d '{"params": {"type": "refund", "amount": "5000"}}'

The outcome lands on the transaction as refund_id and refund_msg. A refund does not rewrite the original status: a fully returned settlement becomes reversed.

Two-Phase Payment (Authorize + Capture)​

Some providers support authorization (hold) followed by capture (charge). The platform holds funds first, then captures when you confirm.

authorize → charging (funds held) → commit → charged

To manually commit an authorized transaction:

curl -X POST https://4pay.online/api/v1/transactions/commit/TX_ID \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444" \
-H "Content-Type: application/json" \
-d '{"params": {"money_storage_meta": {}}}'

Until you commit, the money is held and not settled. If the authorisation expires the hold is released and the transaction fails — a hold is never captured for more than it reserved.

Cascade Routing​

When a provider fails, the platform automatically retries with the next available provider:

Provider A (priority 0) → failed → rollback hold
↓
Provider B (priority 1) → failed → rollback hold
↓
Provider C (priority 2) → charged ✅

Cascade is transparent to the partner. You always receive the final result via webhook. Configure routing priorities in the Admin Console under Terminals & Routing.

Amount Fields​

Every transaction response includes three amount fields:

FieldDescriptionExample
amountOriginal requested amount"10000"
amount_with_feeAmount including all fees"10500"
amount_destAmount received by destination"9500"

For payments: customer pays amount, merchant receives amount_dest. For payouts: merchant sends amount, recipient receives amount_dest.

All amounts are decimal strings of minor units (cents, kopecks). For example, "10000" = 100.00 USD. They are strings because assets with eighteen decimals do not fit a JSON number safely.

Webhook Notifications​

You receive webhooks at each status change. See Webhooks for setup details.

Example webhook payload:

{
"event_id": "4d9d0e6c-1d0a-4a2e-9a2f-0c9a1b0e8f21",
"timestamp": 1772668800,
"type": "transaction.charged",
"data": {
"type": "payment",
"txid": "order_123",
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "charged",
"amount": "10000",
"amount_dest": "9500",
"amount_with_fee": "10500"
}
}
Idempotency

Deduplicate on event_id, not on the transaction id — one transaction produces several events over its life, and the platform may send the same event more than once.

Recovery & Resilience​

  • Transaction state is persisted at every processing stage
  • If a node crashes mid-flow, recovery automatically resumes from the last checkpoint
  • No manual intervention required — the platform guarantees at-least-once delivery

Next Steps​