Quick Start — Your First Payment in 15 Minutes
This guide takes a payment from nothing to settled. No prior knowledge of the platform required.
Prerequisites
Your operator issues two values together, and you need both:
- an API key (Partner Area → Settings → API Keys);
- the organization id the key lives in.
Plus curl, or any HTTP client. Faster still: the SDK does the same four
calls in a dozen lines.
Integrate against https://sandbox.4pay.online and switch the host when you go live. The sandbox
is the same platform with simulated providers — see Sandbox & Testing.
Step 1 — Check your connection
curl https://sandbox.4pay.online/api/v1/health
{
"core_status": "ok",
"db_status": "ok",
"appversion": "f0175d14",
"env": "sandbox",
"domain": "sandbox.4pay.online"
}
Readiness is core_status. There is no field called status.
Step 2 — Create a payment
curl -X POST https://sandbox.4pay.online/api/v1/transactions \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: YOUR_ORGANIZATION_ID" \
-d '{
"params": {
"type": "payment",
"amount": "1000",
"currency": "USD",
"txid": "order-12345",
"env": "test",
"hosted": true,
"returnUrl": "https://yoursite.com/success",
"failUrl": "https://yoursite.com/fail"
}
}'
hosted has no default — set itLeave hosted out and the request is read as server-to-server: the platform looks for card
details, finds none, and answers 201 Created with "status": "rejected" and
"error_description": "unexpected card". This is the single most common reason a first
integration produces transactions that never charge anyone.
amount is a string holding the smallest currency unit (cents, pence,
kopecks). "1000" = $10.00 USD. It travels as a string because assets with
eighteen decimals do not fit a JSON number safely — send "1000", not 1000.
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.sandbox.4pay.online?id=550e8400-...&org_id=...&token=eyJhbGciOi..."
}
Redirect the payer to widget_url — the hosted payment page, where they enter their card. Your
server never touches card data, which is what keeps you out of PCI DSS scope.
widget_url expires in about 15 minutes. Its token is a short-lived JWT. Redirect
immediately; never store the URL or email it for later. If the payer comes back tomorrow, create
a new transaction.
201 Created does not mean accepted. Routing, limits and anti-fraud all refuse a payment by
creating it as rejected. Read status on every response, not just the HTTP code.
Timestamps are not ISO 8601. started and finished arrive as
2026-09-03 11:18:30.101839 Etc/UTC — a space instead of T, a named zone. Most date parsers
return an invalid date; some silently read it as local time. The SDK
handles it.
Step 3 — Check the transaction status
curl https://sandbox.4pay.online/api/v1/transactions/550e8400-e29b-41d4-a716-446655440000 \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: YOUR_ORGANIZATION_ID"
Both headers, every time. The key is resolved inside the organization the second header names, and a request carrying only the key is refused.
Transaction statuses:
| Status | Final | Meaning |
|---|---|---|
created, new | no | Created, not yet routed |
authorizing | no | Being authorised |
action_required | no | The payer has to do something |
charging | no | Being processed by the provider |
charged | yes | Settled ✅ |
rejected | yes | Refused before or by the provider — funds released |
failed | yes | Processing error, or cancelled before settlement — funds released |
reversed | yes | A settled payment was returned |
refunded | no | Refund completed |
Act on final statuses only, and treat any status you do not recognise as non-final — the list is not closed, rails carry their own intermediate states. The full status model is in the protocol specification.
Step 4 — Watch it settle
In the sandbox you can charge a card directly and watch the whole lifecycle, without a browser:
curl -X POST https://sandbox.4pay.online/api/v1/transactions \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-H "x-organization-id: YOUR_ORGANIZATION_ID" \
-d '{
"params": {
"type": "payment",
"amount": "1000",
"currency": "USD",
"txid": "order-12346",
"env": "test",
"show_widget": false,
"money_storage": {
"cardnumber": "2201382000000013",
"cvv": "283",
"cardholder": "IVAN IVANOV",
"exp_year": "20",
"exp_month": "12"
},
"returnUrl": "https://yoursite.com/success",
"failUrl": "https://yoursite.com/fail"
}
}'
It comes back charging, and reaches charged on its own within about a minute. Poll Step 3, or
take the webhook below.
Number, CVV, holder and expiry are compared as one tuple. IVAN IVANOV in capitals, exp_year
of "20" — any difference at all gives rejected / unexpected card. The full set of cards is
in Sandbox & Testing.
A server-to-server payment also needs both returnUrl and failUrl. Without them the
platform creates the transaction and immediately rejects it with return_url/fail_url params not specified. Hosted payments and payouts do not need them.