Code Examples
Working examples of the operations most integrations need. Every one of them has been run against the sandbox.
Base URL: https://sandbox.4pay.online/api/v1 while integrating, https://4pay.online/api/v1
in production.
The short version
The SDK does all of this in a fraction of the code, and already knows the
things the raw API will not tell you — that hosted has no default, that timestamps are not ISO
8601, that a 201 can carry a refusal. Use it unless you have a reason not to.
With the SDK
TypeScript / Node.js
import { FourPay, TransactionRejectedError } from "@4pay/sdk";
const pay = new FourPay({
apiKey: process.env.FOURPAY_API_KEY,
organizationId: process.env.FOURPAY_ORGANIZATION_ID,
baseUrl: "https://sandbox.4pay.online",
});
try {
const { transaction, paymentUrl } = await pay.createHostedPayment({
type: "payment",
amount: "10.00", // major units are converted for you
currency: "USD",
env: "test",
txid: "order-12345", // your order id, and the idempotency key
returnUrl: "https://yoursite.com/success",
failUrl: "https://yoursite.com/fail",
});
console.log(transaction.id, paymentUrl); // redirect the payer to paymentUrl
} catch (error) {
if (error instanceof TransactionRejectedError) {
console.error(`refused: ${error.reason}`);
} else {
throw error;
}
}
Python
from fourpay import FourPay
from fourpay.errors import TransactionRejectedError
client = FourPay(
api_key=os.environ["FOURPAY_API_KEY"],
organization_id=os.environ["FOURPAY_ORGANIZATION_ID"],
base_url="https://sandbox.4pay.online",
)
try:
transaction, payment_url = client.create_hosted_payment(
amount="10.00",
currency="USD",
env="test",
txid="order-12345",
returnUrl="https://yoursite.com/success",
failUrl="https://yoursite.com/fail",
)
print(transaction["id"], payment_url)
except TransactionRejectedError as error:
print(f"refused: {error.reason}")
PHP
use FourPay\Exception\TransactionRejectedException;
use FourPay\Client;
$pay = new FourPay(
apiKey: getenv('FOURPAY_API_KEY'),
organizationId: getenv('FOURPAY_ORGANIZATION_ID'),
baseUrl: 'https://sandbox.4pay.online',
);
try {
[$transaction, $paymentUrl] = $pay->createHostedPayment([
'amount' => '10.00',
'currency' => 'USD',
'env' => 'test',
'txid' => 'order-12345',
'returnUrl' => 'https://yoursite.com/success',
'failUrl' => 'https://yoursite.com/fail',
]);
echo $transaction['id'], ' ', $paymentUrl;
} catch (TransactionRejectedException $error) {
echo 'refused: ', $error->reason();
}
Without the SDK
Four rules the raw examples below all follow, and any integration has to:
- Both headers on every request —
x-api-keyandx-organization-id. The key alone is refused: the platform looks the key up inside the organization the header names, so without it there is nothing to find. The refusal arrives as401on the payment endpoints and400on others — treat both as "these credentials are not usable". hosted: trueon a payment, unless you are charging a card server-to-server.envinsideparams— a?env=query parameter does not set it.- Check
status, not just the HTTP code — a refusal arrives as201 Created.
Python
import requests
API_KEY = "YOUR_API_KEY"
ORG_ID = "00000000-1111-2222-3333-444444444444"
BASE_URL = "https://sandbox.4pay.online/api/v1"
HEADERS = {
"x-api-key": API_KEY,
"x-organization-id": ORG_ID, # not optional
"Content-Type": "application/json",
}
response = requests.post(
f"{BASE_URL}/transactions",
headers=HEADERS,
json={
"params": {
"type": "payment",
"amount": "1000", # minor units, as a string
"currency": "USD",
"txid": "order-12345",
"env": "test",
"hosted": True, # no default — without it the payment is rejected
"returnUrl": "https://yoursite.com/success",
"failUrl": "https://yoursite.com/fail",
}
},
)
tx = response.json()
if tx["status"] in ("rejected", "failed"):
raise RuntimeError(f"payment refused: {tx['error_description']}")
print(f"Payment URL: {tx['widget_url']}") # expires in ~15 minutes
Check the status
tx = requests.get(f"{BASE_URL}/transactions/{tx_id}", headers=HEADERS).json()
print(tx["status"]) # charged, rejected, failed, charging …
List transactions
page = requests.get(
f"{BASE_URL}/transactions",
headers=HEADERS,
params={"env": "test", "status": "charged", "limit": 50},
).json()
for transaction in page["data"]:
print(transaction["id"], transaction["amount"], transaction["currency"])
# Next page: pass the cursor back as `cursor` (NOT `start_cursor`, NOT `page`)
next_cursor = page["cursor"]["cursor_value"]
Issue a refund
refund = requests.put(
f"{BASE_URL}/transactions/{tx_id}",
headers=HEADERS,
json={"params": {"type": "refund", "amount": "500"}}, # omit amount for a full refund
).json()
print(refund["status"])
Create a payout
payout = requests.post(
f"{BASE_URL}/transactions",
headers=HEADERS,
json={
"params": {
"type": "payout",
"amount": "5000",
"currency": "RUB",
"txid": "payout-001",
"env": "test",
"money_storage": {"cardnumber": "2201382000000013"},
}
},
).json()
print(payout["id"], payout["status"])
Webhook handler (Flask)
import hashlib
import hmac
import json
import os
import time
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"]
@app.route("/webhooks/payment", methods=["POST"])
def webhook():
raw = request.get_data() # raw bytes — the signature covers these
timestamp = request.headers.get("x-webhook-timestamp", "")
signature = request.headers.get("x-webhook-signature", "")
expected = hmac.new(
SECRET.encode(), f"{timestamp}.{raw.decode()}".encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return "", 401
if abs(time.time() - int(timestamp or 0)) > 300:
return "", 401
envelope = json.loads(raw)
data = envelope["data"]
# if already_handled(envelope["event_id"]): return "", 200
if data["status"] == "charged":
print(f"Order {data['txid']} paid successfully")
elif data["status"] in ("rejected", "failed"):
print(f"Order {data['txid']} failed: {data.get('error_description')}")
return "", 200
if __name__ == "__main__":
app.run(port=3000)
Node.js
const API_KEY = "YOUR_API_KEY";
const ORG_ID = "00000000-1111-2222-3333-444444444444";
const BASE_URL = "https://sandbox.4pay.online/api/v1";
const headers = {
"x-api-key": API_KEY,
"x-organization-id": ORG_ID, // not optional
"Content-Type": "application/json",
};
const response = await fetch(`${BASE_URL}/transactions`, {
method: "POST",
headers,
body: JSON.stringify({
params: {
type: "payment",
amount: "1000",
currency: "USD",
txid: "order-12345",
env: "test",
hosted: true,
returnUrl: "https://yoursite.com/success",
failUrl: "https://yoursite.com/fail",
},
}),
});
const tx = await response.json();
if (tx.status === "rejected" || tx.status === "failed") {
throw new Error(`payment refused: ${tx.error_description}`);
}
console.log(`Payment URL: ${tx.widget_url}`);
Check the status
const tx = await (
await fetch(`${BASE_URL}/transactions/${txId}`, { headers })
).json();
console.log(tx.status);
Webhook handler (Express)
const express = require("express");
const crypto = require("crypto");
const app = express();
app.post(
"/webhooks/payment",
express.raw({ type: "application/json" }), // raw body — needed for the signature
(req, res) => {
const timestamp = req.get("x-webhook-timestamp");
const signature = req.get("x-webhook-signature") ?? "";
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`)
.digest("hex");
// Compare lengths first: timingSafeEqual throws on buffers of different
// sizes, so a forged header would give a 500 with a stack trace, not a 401.
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
if (!ok) return res.sendStatus(401);
const { event_id, data } = JSON.parse(req.body);
console.log(`Transaction ${data.txid} (${data.id}): ${data.status}`);
res.sendStatus(200);
},
);
app.listen(3000);
PHP
<?php
$apiKey = 'YOUR_API_KEY';
$orgId = '00000000-1111-2222-3333-444444444444';
$baseUrl = 'https://sandbox.4pay.online/api/v1';
$curl = curl_init("{$baseUrl}/transactions");
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"x-api-key: {$apiKey}",
"x-organization-id: {$orgId}", // not optional
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'params' => [
'type' => 'payment',
'amount' => '1000',
'currency' => 'USD',
'txid' => 'order-12345',
'env' => 'test',
'hosted' => true,
'returnUrl' => 'https://yoursite.com/success',
'failUrl' => 'https://yoursite.com/fail',
],
]),
]);
$tx = json_decode(curl_exec($curl), true);
if (in_array($tx['status'], ['rejected', 'failed'], true)) {
throw new RuntimeException("payment refused: {$tx['error_description']}");
}
echo "Payment URL: {$tx['widget_url']}";
What to read next
| Every field, every filter | Transactions |
| Signature verification in full | Webhooks |
| The SDK, including agentic commerce | SDK |