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 mode | Live mode | |
|---|---|---|
| Key prefix | zp_test_… | zp_live_… |
| Money moves | Never | Yes: paid into your wallet |
| Pay page | Buttons to simulate a successful or failed payment | Hosted checkout with the payment provider your platform has connected |
| Wallets | Separate sandbox wallets with play money | Separate live wallets with real money |
| Availability | Every account, instantly | After an administrator approves your account |
| Webhook addresses | http or https | https 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."] } }
| Status | Meaning |
|---|---|
200, 201 | Success; 201 when something was created |
401 | Missing, wrong or revoked key |
403 | Account suspended, or live mode not approved |
404 | No such object in this mode |
422 | Invalid input |
429 | Too 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": [] }Payment links
A payment link is a hosted page where customers pay you. Each link has a shareable url and an 8-character code that customers can also type at https://zpay.okao.site/pay.
POST /payment-links
| Field | |
|---|---|
title | Required. Up to 120 characters. |
amount | Optional decimal such as "25.00". Leave it out to let the customer choose. Between 1.00 and 5,000.00. |
currency | Required. USD or HTG. Live card payments currently support USD. |
description | Optional. Up to 500 characters. |
success_url | Optional. Customers get a Continue button after paying; ?payment=pay_… is added. Live links need https. |
curl https://zpay.okao.site/api/v1/payment-links \
-H "Authorization: Bearer zp_test_your_key_here" \
-H "Idempotency-Key: order-1001" \
-H "Content-Type: application/json" \
-d '{ "title": "Order #1001", "amount": "25.00", "currency": "USD" }'
HTTP 201
{ "data": {
"id": "plink_x8d2k4m7q1w9z3t5v6bn", "object": "payment_link", "mode": "test", "status": "active",
"title": "Order #1001", "description": null, "amount": "25.00", "currency": "USD", "success_url": null,
"code": "K7M2QX9P", "url": "https://zpay.okao.site/pay/K7M2QX9P", "created": 1790000000 } }
GET /payment-links
Lists links, newest first. Optional filter: ?status=active or inactive.
GET /payment-links/{id}
Retrieves one link.
POST /payment-links/{id}/deactivate
Stops a link accepting payments. Existing payments are unaffected.
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/K7M2QX9Pby message, email or QR image. - Payment code: the customer opens
https://zpay.okao.site/payand typesK7M2QX9P. 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.
| Event | Sent when |
|---|---|
payment.succeeded | The customer paid and, in live mode, your wallet was credited |
payment.failed | The payment did not go through |
payment.refunded | The 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
2xxstatus 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 asZPay-Event-Id) to ignore repeats. - Redirects are not followed.
Going live
- Build and test everything with
zp_test_keys and test links. - In the dashboard open API & webhooks and choose Request live access. An administrator reviews and approves your account.
- Create a live key, a live webhook endpoint (https) and live payment links. Store the live secrets separately from the test ones.
- Make one small real payment end to end, check that your wallet is credited and that your server marked the order paid.