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/
Never send card details to this API. A request containing a 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.

ResponseMeaning
401Header missing, or the key does not exist.
403The merchant account is suspended.
429More than 300 requests per minute from this key.

Payment flow

  1. The customer confirms an order on your site.
  2. Your server calls create a payment intent and stores the returned id on the order.
  3. You redirect the customer to the returned payment_link.
  4. The customer pays on the hosted checkout page and is sent back to your return_url.
  5. We call your notification_url as soon as the status changes.
  6. Your handler calls retrieve a payment intent and releases the order if the status is success.
Always treat the webhook plus a retrieve call as the source of truth. A customer can close the browser before being redirected back, and asynchronous payment methods settle minutes later.

Create a payment intent

POST /v1/transaction/payment_intent/

Body

FieldTypeDescription
transaction_refstringrequiredYour own order reference. Must be unique for your merchant account; reusing one is rejected.
client_refstringrequiredYour identifier for the customer (id, email, cart id).
amountstringrequiredMajor units with up to two decimals, e.g. "49.90".
amount_currencystringrequiredISO-4217 code. See supported currencies.
methodstringoptionalDefaults to card.
customer.first_namestringrequired
customer.last_namestringrequired
customer.emailstringrequiredReceipts and payment provider risk checks use this.
customer.phonestringoptional
address.countrystringrequiredTwo-letter billing country.
address.street, city, state, zipstringoptionalImproves authorisation rates.
callback.return_urlurlrequiredWhere the customer lands after paying.
callback.cancel_urlurloptionalWhere "cancel" on the checkout page leads.
callback.notification_urlurloptionalServer-to-server notification endpoint. May contain tags.
session.ip_address, session.user_agentstringoptionalThe 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[]arrayoptionalEach item needs name, quantity, unit_price and sku.
metadataobjectoptionalFree-form key/value data, returned on every read.
force_routestringoptionalPin 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

GET /v1/transaction/payment_intent/{id}/

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"
}
Release the order when 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

GET /v1/transaction/payment_intent/
Query parameterFilters on
transaction_refExact order reference.
client_refExact customer reference.
client_emailExact customer email.
domainSite the intent originated from.
gateway_idPayment provider reference of a related payment.
route_idRoute that took the payment.
statusAny value from statuses.
searchPartial match on order reference, customer reference or email.
pagePage number, 25 per page.
{
  "count": 128,
  "next": "https://pay.0def.com/v1/transaction/payment_intent?page=2",
  "previous": null,
  "results": [ { ... }, { ... } ]
}

Payments

GET /v1/transaction/payment/
GET /v1/transaction/payment/{id}/

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

POST /v1/transaction/refund/
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"
  }'
FieldDescription
paymentrequiredPayment id (not the payment intent id).
amountoptionalPartial amount. Defaults to everything still refundable.
reasonoptionalFree 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.

The notification body carries no payment data and is not signed, so never trust it on its own. Use it purely as a trigger: look the intent up with 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

StatusOrder may be releasedMeaning
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

CodeBodyWhat 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:

Nothing else changes between test and live: same API, same checkout, same webhooks. Only the key.

Handing this to another developer? The same guide is available as a single Markdown file: integration.md.

WooCommerce

For WordPress/WooCommerce stores there is nothing to code:

  1. Download the plugin.
  2. Plugins → Add New → Upload Plugin, then activate it.
  3. WooCommerce → Settings → Payments → 0def Pay, enable it and paste your API key.
  4. 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.

Download WooCommerce plugin

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);