Z PayAPI documentationDashboard →

Z Pay API

The Z Pay API lets you create payment links, follow payments and receive events when customers pay. It is a REST API: it uses JSON, standard HTTP verbs and status codes, and Bearer-token authentication.

Base URL

https://zpay.okao.site/api/v1

Every request and response body is JSON. Amounts are decimal strings such as "25.00"; the same value in the smallest currency unit is available as amount_minor on payments. Timestamps are Unix seconds.

Authentication

Create keys in your dashboard under API & webhooks. Send the key as a Bearer token. Keep keys on your server; never put them in a web page or mobile app.

curl https://zpay.okao.site/api/v1/account \
  -H "Authorization: Bearer zp_test_your_key_here"

A key is shown once when you create it. Z Pay stores only a fingerprint, so a lost key cannot be recovered: revoke it and create a new one.

Test and live mode

Test modeLive mode
Key prefixzp_test_…zp_live_…
Money movesNeverYes: paid into your wallet
Pay pageButtons to simulate a successful or failed paymentHosted checkout with the payment provider your platform has connected
WalletsSeparate sandbox wallets with play moneySeparate live wallets with real money
AvailabilityEvery account, instantlyAfter an administrator approves your account
Webhook addresseshttp or httpshttps only

Objects created in one mode are invisible in the other. A live key used before your account is approved receives a 403 live_not_enabled error.

Errors and limits

Problems with your key or account use this shape:

HTTP 401
{ "error": { "type": "authentication_error", "message": "Provide a valid API key as a Bearer token in the Authorization header." } }

Invalid input returns 422 with a field-by-field list:

HTTP 422
{ "message": "The title field is required.", "errors": { "title": ["The title field is required."] } }
StatusMeaning
200, 201Success; 201 when something was created
401Missing, wrong or revoked key
403Account suspended, or live mode not approved
404No such object in this mode
422Invalid input
429Too many requests: 60 per minute per key. Wait and retry.

List endpoints return 20 items per page with links and meta; follow links.next for more, or pass ?page=2.

Idempotency

Send an Idempotency-Key header (up to 100 characters) when creating a payment link. Repeating the request with the same key returns the original link with status 200 instead of creating a duplicate, so it is safe to retry after a network failure.

Account

GET /account

Returns the account behind the key, the key's mode, and, for live keys, your wallet balances.

{ "object": "account", "name": "Mia Shop", "email": "mia@example.com", "mode": "test", "live_enabled": false, "balances": [] }

Payments

A payment is created when a customer pays through one of your links.

GET /payments

Lists payments, newest first. Filters: ?status=succeeded (pending, succeeded, failed, cancelled) and ?payment_link=plink_….

GET /payments/{id}

{ "data": { "id": "pay_h3k9d2m4q8w1z5t7v6bn", "object": "payment", "mode": "test", "status": "succeeded",
    "amount": "25.00", "amount_minor": 2500, "currency": "USD", "payment_link": "plink_x8d2k4m7q1w9z3t5v6bn",
    "payer_email": "customer@example.com", "provider": "test", "created": 1790000100, "completed": 1790000101 } }

Only treat an order as paid when status is succeeded. Confirm it with a webhook or by retrieving the payment; do not rely on the customer's browser returning to your site.

Webhook endpoints

POST /webhook-endpoints

curl https://zpay.okao.site/api/v1/webhook-endpoints \
  -H "Authorization: Bearer zp_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/webhooks/zpay" }'
HTTP 201
{ "id": "we_p4k8d2m7q1w9z3t5v6bn", "object": "webhook_endpoint", "mode": "test", "url": "https://example.com/webhooks/zpay",
  "active": true, "created": 1790000000, "secret": "whsec_…" }

The secret is returned only in this response. Addresses must be public servers; localhost and private network addresses are refused. You can have 5 endpoints per mode.

GET /webhook-endpoints

Lists endpoints for the key's mode.

DELETE /webhook-endpoints/{id}

Removes an endpoint.

Payment links and payment codes

Every link works three ways, all pointing at the same hosted page:

  • Link: send https://zpay.okao.site/pay/K7M2QX9P by message, email or QR image.
  • Payment code: the customer opens https://zpay.okao.site/pay and types K7M2QX9P. Codes use only easily-read letters and digits, and spaces, dashes and lower case are ignored.
  • API: create and track links programmatically as shown above.

In test mode the page shows two buttons so you can walk through a success and a failure. In live mode customers pay on a secure hosted checkout, and a successful payment credits your live wallet.

Receiving webhooks

Z Pay sends an HTTPS POST to your endpoints when a payment on one of your links finishes.

EventSent when
payment.succeededThe customer paid and, in live mode, your wallet was credited
payment.failedThe payment did not go through
payment.refundedThe merchant refunded a successful payment
{ "id": "evt_5k2d8m4q1w9z3t7v6bnh2c1x", "object": "event", "type": "payment.succeeded", "created": 1790000101, "mode": "test",
  "data": { "object": { "id": "pay_…", "object": "payment", "status": "succeeded", "amount": "25.00", "currency": "USD" } } }

Verify the signature

Each request carries ZPay-Signature: t=<timestamp>,v1=<signature>. The signature is the hex HMAC-SHA256 of timestamp + "." + raw request body, keyed with your endpoint's secret. Reject requests whose signature does not match or whose timestamp is more than 5 minutes old.

// PHP
$header = $_SERVER['HTTP_ZPAY_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);
$payload = file_get_contents('php://input');
$expected = hash_hmac('sha256', $parts['t'].'.'.$payload, $secret);
$valid = hash_equals($expected, $parts['v1'] ?? '') && abs(time() - (int) $parts['t']) <= 300;
// Node.js
const [t, v1] = req.headers['zpay-signature'].split(',').map(p => p.split('=')[1]);
const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now()/1000 - t) <= 300;
# Python
t, v1 = [p.split('=')[1] for p in request.headers['ZPay-Signature'].split(',')]
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
valid = hmac.compare_digest(expected, v1) and abs(time.time() - int(t)) <= 300

Responding and retries

  • Return any 2xx status quickly. Anything else, or no answer within 5 seconds, counts as a failure.
  • Failed deliveries are retried up to 5 times with growing delays (2, 4, 8 and 16 minutes).
  • Delivery is at least once. Use the event id (also sent as ZPay-Event-Id) to ignore repeats.
  • Redirects are not followed.

Going live

  1. Build and test everything with zp_test_ keys and test links.
  2. In the dashboard open API & webhooks and choose Request live access. An administrator reviews and approves your account.
  3. Create a live key, a live webhook endpoint (https) and live payment links. Store the live secrets separately from the test ones.
  4. Make one small real payment end to end, check that your wallet is credited and that your server marked the order paid.