0def Pay API
One gateway for every site you sell from. Your site creates a payment intent, redirects the customer to a hosted checkout page, and gets notified when the money arrives — card data never touches your servers.
Overview
Base URL for every endpoint:
https://pay.0def.com/v1/
- REST over HTTPS, JSON request and response bodies.
- Amounts are decimal strings in major units (
"49.90"), never cents. - Trailing slashes are optional.
- Identifiers are UUIDs.
- List endpoints are paginated with
count,next,previousandresults.
card
object is rejected with HTTP 422. Card data is collected exclusively on the hosted checkout page,
inside the payment provider's own iframe, which is what keeps your site out of PCI scope.
Authentication
Every request carries your merchant API key:
Authorization: Token zdp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keys are issued per site, so each of your stores gets its own. They are shown once when created or rotated and stored only as a hash afterwards. Keep them server-side: a key can create charges.
| Response | Meaning |
|---|---|
401 | Header missing, or the key does not exist. |
403 | The merchant account is suspended. |
429 | More than 300 requests per minute from this key. |
Payment flow
- The customer confirms an order on your site.
- Your server calls create a payment intent and stores the returned
idon the order. - You redirect the customer to the returned
payment_link. - The customer pays on the hosted checkout page and is sent back to your
return_url. - We call your
notification_urlas soon as the status changes. - Your handler calls retrieve a payment intent and releases the order if the status is
success.
Create a payment intent
Body
| Field | Type | Description | |
|---|---|---|---|
transaction_ref | string | required | Your own order reference. Must be unique for your merchant account; reusing one is rejected. |
client_ref | string | required | Your identifier for the customer (id, email, cart id). |
amount | string | required | Major units with up to two decimals, e.g. "49.90". |
amount_currency | string | required | ISO-4217 code. See supported currencies. |
method | string | optional | Defaults to card. |
customer.first_name | string | required | |
customer.last_name | string | required | |
customer.email | string | required | Receipts and payment provider risk checks use this. |
customer.phone | string | optional | |
address.country | string | required | Two-letter billing country. |
address.street, city, state, zip | string | optional | Improves authorisation rates. |
callback.return_url | url | required | Where the customer lands after paying. |
callback.cancel_url | url | optional | Where "cancel" on the checkout page leads. |
callback.notification_url | url | optional | Server-to-server notification endpoint. May contain tags. |
session.ip_address, session.user_agent | string | optional | The shopper's own IP and user agent. Defaults to those of the API call, so pass them explicitly when calling from a backend. |
order_items[] | array | optional | Each item needs name, quantity, unit_price and sku. |
metadata | object | optional | Free-form key/value data, returned on every read. |
force_route | string | optional | Pin the payment to a specific route. |
Callback hosts are validated: they must be your registered site domain, one of its subdomains, or a host explicitly allowed for your account.
Request
curl -X POST https://pay.0def.com/v1/transaction/payment_intent/ \
-H "Authorization: Token $ZERODEF_PAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"transaction_ref": "order-1001",
"client_ref": "customer-77",
"amount": "49.90",
"amount_currency": "EUR",
"method": "card",
"customer": {
"first_name": "Ana",
"last_name": "Nikolic",
"email": "ana@example.com",
"phone": "+381601234567"
},
"address": {
"street": "Knez Mihailova 1",
"city": "Beograd",
"zip": "11000",
"country": "RS"
},
"session": {
"ip_address": "203.0.113.24",
"user_agent": "Mozilla/5.0 ..."
},
"callback": {
"return_url": "https://your-site.com/checkout/thanks?order=1001",
"cancel_url": "https://your-site.com/cart",
"notification_url": "https://your-site.com/pay/notify?intent={payment_intent_id}&order={transaction_id}"
},
"order_items": [
{ "sku": "TS-01", "name": "T-shirt", "quantity": 2, "unit_price": "19.95" },
{ "sku": "SH-99", "name": "Shipping", "quantity": 1, "unit_price": "10.00" }
],
"metadata": { "campaign": "spring" }
}'
Response 201
{
"id": "9b1f2c7a-4d3e-4f18-9a2b-6c5d4e3f2a10",
"client_ref": "customer-77",
"payment_link": "https://pay.0def.com/checkout/kPq3...48-chars"
}
Store id against the order, then redirect the browser to payment_link. The link stays payable for 24 hours.
Retrieve a payment intent
This is the call your webhook handler makes to learn the authoritative status.
{
"id": "9b1f2c7a-4d3e-4f18-9a2b-6c5d4e3f2a10",
"username": "your-merchant-slug",
"transaction_ref": "order-1001",
"client_ref": "customer-77",
"first_name": "Ana",
"last_name": "Nikolic",
"email": "ana@example.com",
"phone": "+381601234567",
"address": "Knez Mihailova 1, Beograd, 11000, RS",
"amount": "49.90",
"currency": "EUR",
"base_amount": "49.90",
"base_currency": "EUR",
"method": "card",
"status": "success",
"payment_link": "https://pay.0def.com/checkout/kPq3...",
"payment": [
{
"id": "2f8c...",
"amount": "49.90",
"currency": "EUR",
"fee": "1.12",
"refunded": "0.00",
"status": "success",
"gateway_id": "pi_3Qx...",
"card": { "brand": "visa", "last4": "4242", "country": "RS" },
"failure_code": null,
"failure_message": null,
"refund": [],
"created": "2026-08-06T10:12:31+00:00"
}
],
"actions": { "pay": null, "return_url": "...", "cancel_url": "..." },
"metadata": { "campaign": "spring" },
"order_items": [ ... ],
"route_id": "3",
"route_name": "EUR cards",
"expires_at": "2026-08-07T10:12:30+00:00",
"created": "2026-08-06T10:12:30+00:00"
}
status is success and the amount
matches what you expect. base_amount and base_currency always mirror the
charged values; no currency conversion happens in this gateway.
List payment intents
| Query parameter | Filters on |
|---|---|
transaction_ref | Exact order reference. |
client_ref | Exact customer reference. |
client_email | Exact customer email. |
domain | Site the intent originated from. |
gateway_id | Payment provider reference of a related payment. |
route_id | Route that took the payment. |
status | Any value from statuses. |
search | Partial match on order reference, customer reference or email. |
page | Page number, 25 per page. |
{
"count": 128,
"next": "https://pay.0def.com/v1/transaction/payment_intent?page=2",
"previous": null,
"results": [ { ... }, { ... } ]
}
Payments
A payment is one attempt to collect a payment intent. Filter with payment_intent_id,
gateway_id or status. The object is the same one embedded in the
payment array of a payment intent.
Refunds
curl -X POST https://pay.0def.com/v1/transaction/refund/ \
-H "Authorization: Token $ZERODEF_PAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"payment": "2f8c1d9e-77aa-4b62-9f0c-1d2e3f4a5b6c",
"amount": "19.95",
"reason": "customer returned one item"
}'
| Field | Description | |
|---|---|---|
payment | required | Payment id (not the payment intent id). |
amount | optional | Partial amount. Defaults to everything still refundable. |
reason | optional | Free text, up to 128 characters. |
Repeated partial refunds are allowed up to the captured amount; going over is rejected with 422.
The payment moves to partial_refunded and then refunded, and each change
triggers a notification. List refunds with
GET /v1/transaction/refund/ (filters: payment, status).
Webhooks
Whenever a payment intent's status changes we send a GET request to your
notification_url and expect HTTP 200. Anything else is retried
10 times: the first retry 30 seconds later, the rest 60 seconds apart.
Tags
Any of these placeholders in your notification_url are substituted:
{payment_intent_id} {transaction_id} {transaction_ref} {client_ref} {status}
With no tags at all, payment_intent_id and transaction_id are appended as query parameters instead.
GET /v1/transaction/payment_intent/{id}/ and act on the status you read back.
Also make the handler idempotent — the same event can arrive more than once.
// GET https://your-site.com/pay/notify?intent=9b1f2c7a-...&order=order-1001
$intent = http_get(
"https://pay.0def.com/v1/transaction/payment_intent/{$_GET['intent']}/",
["Authorization: Token {$key}"]
);
if ($intent['status'] === 'success' && $intent['transaction_ref'] === $order->reference) {
$order->markPaid(); // must be safe to call twice
}
http_response_code(200); // anything else is retried
Statuses
| Status | Order may be released | Meaning |
|---|---|---|
pending |
no | Pending |
partial |
no | Partially Paid |
success |
yes | Success |
failed |
no | Failed |
canceled |
no | Canceled |
expired |
no | Expired |
authorized |
no | Authorized |
refunded |
no | Refunded |
partial_refunded |
yes | Partially Refunded |
disputed |
yes | Disputed |
disputed_won |
yes | Disputed - Won |
disputed_lost |
no | Disputed - Lost |
disputed_close |
no | Disputed - Closed |
Routing
Which acquiring account collects a payment is decided by routing rules configured on our side
(per site, currency, method, billing country or amount band). The chosen route is reported back
as route_id and route_name, and it is what appears on the customer's
bank statement.
To pin a payment to a specific route, send force_route with the route id we gave you.
An unusable route is rejected with 422 rather than silently falling back.
Errors
| Code | Body | What to do |
|---|---|---|
401 / 403 | {"detail": "..."} | Fix the API key; do not retry. |
404 | {"detail": "..."} | Unknown id, or it belongs to another merchant. |
422 | {"message": "...", "errors": {"field": ["..."]}} | Validation or routing problem. Fix the payload. |
429 | {"message": "Too Many Requests"} | Back off and retry. |
502 | {"detail": "...", "provider_message": "..."} | The payment provider refused to open the payment. Safe to retry with the same transaction_ref. |
Testing
Ask us for a test-mode merchant key. Routes then point at the payment providers' sandboxes and no
real money moves; provider test cards (for example 4242 4242 4242 4242 with any future
expiry and CVC) work on the hosted checkout. The API, checkout page and webhook behaviour are
identical in both modes, so only the key changes when you go live.
Supported currencies
AEDAUDBHDCADCHFCLPCZKDKKEURGBPHKDILSINRKWDMXNNOKNZDOMRPHPPLNQARRSDSARSEKUSDZAR
Supported methods
cardpaypalklarnaidealbancontacttrustlyp24blikwire
Availability depends on the route: card works everywhere, the rest on request.
Going live
Before you switch the test key for a live one:
- A test payment completes and the order is released by your own code, not by hand.
- Your notification handler answers
200and survives being called twice with the same URL. - You compare
amountagainst the order total before releasing it. session.ip_addressandsession.user_agentare the shopper's, not your server's.failedandexpiredintents mark the order failed, and the customer can retry with a newtransaction_ref.- Refunds are tested, including a partial one.
- The live key lives in a secret store, and callback URLs point at your production domain over HTTPS.
Nothing else changes between test and live: same API, same checkout, same webhooks. Only the key.
WooCommerce
For WordPress/WooCommerce stores there is nothing to code:
- Download the plugin.
- Plugins → Add New → Upload Plugin, then activate it.
- WooCommerce → Settings → Payments → 0def Pay, enable it and paste your API key.
- Place a test order and confirm it reaches Processing after payment.
The plugin creates the payment intent, redirects to the hosted checkout, handles the return and the notification, and writes debug entries to WooCommerce → Status → Logs when logging is enabled.
PHP example
<?php
function zerodef_pay_request(string $method, string $path, array $body = null): array
{
$ch = curl_init('https://pay.0def.com/v1'.$path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Authorization: Token '.getenv('ZERODEF_PAY_KEY'),
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = json_decode((string) $response, true) ?: [];
if ($status >= 400) {
throw new RuntimeException($decoded['detail'] ?? 'Payment gateway error (HTTP '.$status.')');
}
return $decoded;
}
// 1. Create the intent when the customer confirms the order.
$intent = zerodef_pay_request('POST', '/transaction/payment_intent/', [
'transaction_ref' => $order->reference,
'client_ref' => (string) $order->customer_id,
'amount' => number_format($order->total, 2, '.', ''),
'amount_currency' => $order->currency,
'customer' => [
'first_name' => $order->first_name,
'last_name' => $order->last_name,
'email' => $order->email,
],
'address' => ['country' => $order->country, 'city' => $order->city],
'session' => ['ip_address' => $_SERVER['REMOTE_ADDR'], 'user_agent' => $_SERVER['HTTP_USER_AGENT']],
'callback' => [
'return_url' => 'https://your-site.com/checkout/thanks?order='.$order->reference,
'cancel_url' => 'https://your-site.com/cart',
'notification_url' => 'https://your-site.com/pay/notify?intent={payment_intent_id}',
],
]);
$order->update(['pay_intent_id' => $intent['id']]);
header('Location: '.$intent['payment_link']);
// 2. Confirm the outcome from your notification endpoint or thank-you page.
$state = zerodef_pay_request('GET', '/transaction/payment_intent/'.$order->pay_intent_id.'/');
if ($state['status'] === 'success') {
$order->markPaid();
}
Node example
const BASE = 'https://pay.0def.com/v1';
async function pay(method, path, body) {
const response = await fetch(BASE + path, {
method,
headers: {
Authorization: `Token ${process.env.ZERODEF_PAY_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.detail || `Payment gateway error (HTTP ${response.status})`);
}
return data;
}
const intent = await pay('POST', '/transaction/payment_intent/', {
transaction_ref: order.reference,
client_ref: String(order.customerId),
amount: order.total.toFixed(2),
amount_currency: order.currency,
customer: { first_name: order.firstName, last_name: order.lastName, email: order.email },
address: { country: order.country },
callback: {
return_url: `https://your-site.com/checkout/thanks?order=${order.reference}`,
notification_url: 'https://your-site.com/pay/notify?intent={payment_intent_id}',
},
});
res.redirect(intent.payment_link);