Skip to main content

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:

  1. Both headers on every request — x-api-key and x-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 as 401 on the payment endpoints and 400 on others — treat both as "these credentials are not usable".
  2. hosted: true on a payment, unless you are charging a card server-to-server.
  3. env inside params — a ?env= query parameter does not set it.
  4. Check status, not just the HTTP code — a refusal arrives as 201 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']}";

Every field, every filterTransactions
Signature verification in fullWebhooks
The SDK, including agentic commerceSDK