Transactions
The transaction is the core object of the 4pay.online API. Every payment, payout, and refund is a transaction.
Base URL: https://4pay.online/api/v1/transactions
Create a transaction
POST /api/v1/transactions
Request body
{
"params": {
"type": "payment",
"amount": "1000",
"currency": "USD",
"txid": "order-12345",
"env": "test",
"hosted": true,
"returnUrl": "https://yoursite.com/success",
"failUrl": "https://yoursite.com/fail"
}
}
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
type | string | ✅ | Transaction type: payment, payout |
amount | string | ✅ | Amount in smallest currency unit, as a decimal string ("1000" = 10.00 USD) |
currency | string | ✅ | ISO 4217 currency code (USD, EUR, USDT, …) |
txid | string | — | Your order ID. Free-form; unique per partner. Enables idempotency — see below |
returnUrl | string | — | Where the payer lands after paying. Required for a server-to-server payment; optional for hosted payments and payouts |
failUrl | string | — | Where the payer lands after a failure. Same rule as returnUrl |
env | string | ✅ | prod or test. Must be inside params — see below |
hosted | boolean | — | true sends the payer to the hosted page. No default — see below. show_widget is an older alias for the same thing |
description | string | — | Payment description shown to the payer |
customer | object | — | Payer info: { email, phone, name } |
lang | string | — | UI language: en, ru, ar, es, … |
merchant_meta | object | — | Your opaque data, returned on reads and in webhooks unchanged |
Response
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "created",
"type": "payment",
"env": "test",
"txid": "order-12345",
"amount": "1000",
"currency": "USD",
"started": "2026-09-03 11:18:30.101839 Etc/UTC",
"widget_url": "https://pay.4pay.online?id=550e8400-...&org_id=...&token=eyJhbGciOi..."
}
Redirect your user to widget_url to complete the payment. The token in it is a JWT valid for
about 15 minutes — redirect promptly, and create a new transaction rather than storing the URL.
hosted has no defaultA create without hosted: true (or show_widget: true) is treated as server-to-server: the
platform expects card details in money_storage, and without them answers 201 Created with
"status": "rejected" and "error_description": "unexpected card". Nothing in the HTTP status
tells you the payment was refused.
A server-to-server payment additionally requires both returnUrl and failUrl; without them
it is created and immediately rejected with return_url/fail_url params not specified.
started and finished come back as 2026-09-03 11:18:30.101839 Etc/UTC — a space where T
belongs, six fractional digits, a named zone. new Date(...) in JavaScript returns an invalid
date; datetime.fromisoformat in Python raises; PHP reads a zoneless value in the server's own
timezone. The SDK parses it correctly in all three.
Idempotency
txid is any string you choose, and it is unique per partner. Repeating a
create request with a txid that already exists does not create a second
transaction — you get the existing one back, again with 201 and a location
header pointing at it. Two partners inside one organization may reuse the same
txid without colliding.
A transaction created without txid has no idempotency at all: a retry
after a timeout produces a second transaction, and therefore a second payment.
Send a txid for anything that involves money.
Constraints
| Field | Constraint |
|---|---|
txid | Up to 255 characters. A longer value is refused with 422, not a database error |
txid | Case-sensitive and stored exactly as sent — ORDER-abc and order-abc are two different transactions |
amount | Must be greater than zero. "0" and a negative value are both refused with 422 |
amount | Arbitrary-precision decimal string — an 18-digit value (1 ETH in wei) is accepted as-is |
txidRepeating a create with the same txid but a different amount still returns the original
transaction, unchanged, at its original amount. The new amount is not applied and no error is
raised — read the response instead of assuming the latest amount took effect.
Environment
Send env inside params. A ?env= query parameter selects the
environment a listing or lookup searches; it does not set the environment of a
transaction being created. A create request carrying only ?env=test produces a
production transaction. When the payment
needs something from the payer mid-flow instead, the action arrives in
auth_params — see the
Universal Transaction Protocol.
Get a transaction
GET /api/v1/transactions/:id
curl https://4pay.online/api/v1/transactions/550e8400-e29b-41d4-a716-446655440000 \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444"
Response
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "charged",
"txid": "order-12345",
"amount": "1000",
"amount_with_fee": "1050",
"amount_dest": "950",
"currency": "USD",
"currency_dest": "USD",
"started": "2026-03-01T10:00:00Z",
"finished": "2026-03-01T10:05:00Z"
}
Amount fields
| Field | Description |
|---|---|
amount | Original transaction amount (what the payer pays) |
amount_with_fee | Amount including platform fee |
amount_dest | Amount credited to you (after fee deduction) |
List transactions
GET /api/v1/transactions
curl "https://4pay.online/api/v1/transactions?status=charged&limit=50" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444"
Query parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status |
type | string | Filter by type: payment, payout |
env | string | prod or test — which environment to search |
limit | integer | Results per page (default: 20, max: 100) |
cursor | string | cursor_value from the previous response — see paging below |
started_from, started_to | ISO 8601 | Bounds on creation time |
updated_from, updated_to | ISO 8601 | Bounds on last update |
finished_from, finished_to | ISO 8601 | Bounds on completion |
amount_from, amount_to | string | Bounds on amount, in minor units |
order_by | string | started (default) or updated |
rrn | string | Retrieval reference number |
link_id | uuid | Payments made through one payment link |
Anything not in that table is refused with 422 Unexpected field — including page, offset,
from, to and currency, all of which look plausible and none of which exist.
Paging
Responses carry a cursor block; pass its cursor_value back as cursor to get the next page.
{
"data": [ … ],
"cursor": { "cursor_field": "started", "cursor_value": "2026-09-03T11:20:45.377937Z" }
}
curl "https://4pay.online/api/v1/transactions?env=test&limit=50&cursor=2026-09-03T11:20:45.377937Z" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: YOUR_ORGANIZATION_ID"
A page whose cursor_value is null is the last one.
start_cursor is accepted and then ignored unless end_cursor comes with it (that pair is a
windowed read). A loop built on start_cursor alone re-reads page one for ever, and looks exactly
like "there is nothing more".
txid is likewise accepted from a partner key and ignored, returning your whole list rather than
one order. Keep the platform id you got from the create call and read it back with
GET /api/v1/transactions/:id.
Aggregate transactions
Get summarized stats across multiple transactions:
GET /api/v1/transactions/aggregate
curl "https://4pay.online/api/v1/transactions/aggregate?currency=USD&status=charged" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444"
Update a transaction
Use PUT /api/v1/transactions/:id to issue a refund or cancel a transaction.
Refund
curl -X PUT https://4pay.online/api/v1/transactions/550e8400-... \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444" \
-d '{
"params": {
"type": "refund",
"amount": "500"
}
}'
Omit amount for a full refund.
Cancel
curl -X PUT https://4pay.online/api/v1/transactions/550e8400-... \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444" \
-d '{
"params": {
"type": "cancel"
}
}'
Two-phase payments (Authorize + Capture)
For providers that support two-phase, first create with type: "payment" — the funds are held but not captured. Then commit:
POST /api/v1/transactions/commit/:id
curl -X POST https://4pay.online/api/v1/transactions/commit/550e8400-... \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: 00000000-1111-2222-3333-444444444444"
3DS / Additional authentication
When a transaction needs additional authentication (3-D Secure, a one-time
password, a wallet signature), its status becomes action_required and the
transaction carries the action itself in auth_params — there is no
action_required boolean. Render the action as described in the
protocol specification.
Material the payer must sign or complete arrives in
additional_params_for_auth, with an expires_at after which it must be
requested again. Submit the result with:
POST /api/v1/transactions/additional_params_for_auth/:id
{
"params": {
"otp": "123456"
}
}
Transaction lifecycle
created
└─► charging processing
├─► action_required payer action needed, then back to charging
├─► charged ✅ settled
├─► rejected ❌ no routing, limits or anti-fraud
└─► failed ❌ provider error, or cancelled before settlement
└─► cancelled cancelled before processing started
charged
├─► reversed a settled transaction was returned
└─► refunded refund completed
charged, rejected, failed and reversed are final. Everything else means
the transaction is still in flight.
reversed can only follow charged — it is the return of a settled
transaction, not a cancellation. A cancellation before settlement arrives as
failed.