SDK
Official clients for TypeScript, Python and PHP. Same API in all three, same behaviour, same tests.
The SDK exists because the raw API has a handful of behaviours that are easy to get wrong and
expensive when you do — a 201 Created that carries a refusal, a hosted flag with no default,
timestamps that are not ISO 8601, a paging parameter that is accepted and ignored. Every one of
those is handled here, and every one is documented in the code where you will meet it.
Install
The packages are not on npm, PyPI or Packagist. Each installs from its own public repository — one per language, since npm and Composer read a manifest only from the root of a repository — or from the archive this site serves, for a build with no access to GitHub.
TypeScript / Node.js 18+
npm install github:4pay-online/sdk-typescript
Or from the archive:
curl -O https://docs.4pay.online/sdk/4pay-sdk-typescript.tar.gz
npm install ./4pay-sdk-typescript.tar.gz
dist/ is committed, so neither install compiles anything.
Python 3.9+
pip install "git+https://github.com/4pay-online/sdk-python.git"
# or: pip install https://docs.4pay.online/sdk/4pay-sdk-python.tar.gz
The SDK has no runtime dependencies. Mandate signature verification (AP2) needs one extra:
pip install "4pay-sdk[crypto] @ git+https://github.com/4pay-online/sdk-python.git"
PHP 8.1+
Composer reaches the repository through a vcs entry:
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/4pay-online/sdk-php.git" }
],
"require": { "4pay/sdk": "dev-master" }
}
composer install
Or unpack the archive and run composer install inside it. Requires ext-json, ext-curl and
ext-bcmath.
Sources: github.com/4pay-online — TypeScript, Python and PHP, MIT.
All three archives: docs.4pay.online/sdk/ — sources, examples and tests included.
First payment
import { FourPay } from "@4pay/sdk";
const pay = new FourPay({
apiKey: process.env.FOURPAY_API_KEY,
organizationId: process.env.FOURPAY_ORGANIZATION_ID, // issued together with the key
baseUrl: "https://sandbox.4pay.online",
});
const { transaction, paymentUrl } = await pay.createHostedPayment({
type: "payment",
amount: "150.00", // or "15000" in minor units — both work
currency: "USD",
env: "test",
txid: "order-8891", // your order id, and the idempotency key
returnUrl: "https://shop.example/thanks",
failUrl: "https://shop.example/sorry",
});
redirect(paymentUrl); // valid for ~15 minutes
from fourpay import FourPay
client = FourPay(
api_key=os.environ["FOURPAY_API_KEY"],
organization_id=os.environ["FOURPAY_ORGANIZATION_ID"],
base_url="https://sandbox.4pay.online",
)
transaction, payment_url = client.create_hosted_payment(
amount="150.00", currency="USD", env="test", txid="order-8891",
returnUrl="https://shop.example/thanks", failUrl="https://shop.example/sorry",
)
$pay = new FourPay\Client(
apiKey: getenv('FOURPAY_API_KEY'),
organizationId: getenv('FOURPAY_ORGANIZATION_ID'),
baseUrl: 'https://sandbox.4pay.online',
);
[$transaction, $paymentUrl] = $pay->createHostedPayment([
'amount' => '150.00', 'currency' => 'USD', 'env' => 'test', 'txid' => 'order-8891',
'returnUrl' => 'https://shop.example/thanks', 'failUrl' => 'https://shop.example/sorry',
]);
What it handles for you
| Behaviour of the raw API | What the SDK does |
|---|---|
hosted has no default; without it a payment is refused as unexpected card | createHostedPayment sets it |
201 Created carries refusals (rejected, failed) | Raises TransactionRejectedError, with isRoutingFailure to tell "no terminal" from "card declined" |
A missing x-organization-id is refused with 401 on the payment endpoints and 400 on others | Both headers always sent; a bare 400 or 401 surfaces as an auth error |
Timestamps like 2026-09-03 11:18:30.101839 Etc/UTC | parseTimestamp returns a correct UTC date in all three languages |
Paging is cursor; start_cursor is silently ignored | iterateTransactions follows the real cursor, and stops if it stops moving |
page, offset, from, to, currency are refused with 422 | Not in the type definitions, so your editor refuses them first |
A retried create without txid charges twice | Writes are only retried when they carry an idempotency key |
| Money as decimal strings in minor units | toMinorUnits / fromMinorUnits, integer arithmetic throughout — never floats |
Webhook signature over <timestamp>.<raw body> | verifyWebhook — constant-time, replay-windowed, raw-bytes |
Webhooks
import { verifyWebhook, EventDeduplicator } from "@4pay/sdk";
const seen = new EventDeduplicator();
app.post("/webhooks/payment", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.headers, process.env.WEBHOOK_SECRET);
} catch {
return res.sendStatus(401);
}
res.sendStatus(200); // acknowledge first, work afterwards
if (!seen.accept(event.event_id)) return; // deliveries repeat
if (event.data.status === "charged") fulfil(event.data.txid);
});
The raw body matters: the signature covers bytes, and a parsed-then-re-serialised body has
different ones. In Express that means express.raw() on this route; in Flask
request.get_data(); in PHP file_get_contents('php://input').
Money
toMinorUnits("10.00", "USD") // "1000"
toMinorUnits("10.001", "USD") // throws — USD has two decimals, and rounding money is not ours to do
toMinorUnits(10.5, "USD") // throws — pass "10.50", not a float
fromMinorUnits("1000", "JPY") // "1000" — JPY has no minor unit
formatAmount("2500", "EUR") // "25.00 EUR"
An unknown currency raises rather than assuming two decimals: a guessed scale overstates JPY a hundredfold and ETH by sixteen orders of magnitude.
Sandbox fixtures
import { sandbox } from "@4pay/sdk";
sandbox.CARD_CHARGE // settles
sandbox.CARD_REJECT // refused by the provider
sandbox.CARD_REFUND // settles, then refunds
sandbox.CARD_FAIL // fails
The simulator matches the whole tuple — number, CVV, holder, expiry — so these constants save you
from a rejected caused by a lower-case cardholder name. See
Sandbox & Testing.
Agentic commerce
The SDK also carries the merchant side of the four protocols AI agents use to pay — MCP, AP2, ACP and A2A. That has its own page.
Examples
The archive ships runnable examples:
| Example | What it shows |
|---|---|
first-payment/ | The whole lifecycle end to end, in all three languages |
webhook-server/ | A verified webhook endpoint, in all three languages |
agent-tools/ | An AI agent taking payments through function calling |
acp-merchant/ | An ACP checkout served from your own endpoints |
a2a-agent/ | A payment agent other agents can hire |
ap2-checkout/ | Verifying a mandate before charging |
Support
- Source, issues and changelog: the archive's
README.md - Platform questions: support@4pay.online — include the transaction
id