Overview
Payavex is a private gateway: it serves websites that have been registered by its operator. Each registered website gets its own API key, its own signing secret and its own callback URL, and can only ever see its own invoices.
A payment runs in three moves. Your server creates an invoice and gets a checkout URL. You send the customer there — in a new page or in a popup on your own page. Your server updates the order when the signed callback arrives, or when it asks for the invoice status itself.
All requests go to https://payavex.com over HTTPS. Request and response bodies are JSON. Amounts are decimal strings, never floats.
Authentication
Every request carries three headers. There is no bearer token and no session.
| Header | Value |
|---|---|
X-Api-Key | Your website’s API key. |
X-Timestamp | Current Unix time in seconds. Rejected if more than 5 minutes from our clock. |
X-Signature | Lowercase hex HMAC-SHA256 of <timestamp>.<signed string> using your API secret. |
The signed string is the raw request body for POST requests, and the request path for GET requests. Sign the exact bytes you send — re-encoding the JSON after signing will break the signature.
// body to sign: POST -> the raw JSON body; GET -> the request path
$ts = time();
$body = json_encode(['order_id' => 'ORDER-1001', 'amount' => '25.00']);
$sig = hash_hmac('sha256', $ts . '.' . $body, $API_SECRET);
// request headers
X-Api-Key: YOUR_API_KEY
X-Timestamp: $ts
X-Signature: $sigCreate a payment
POST /api/v1/invoice — returns the invoice and its checkout_url.
| Field | Required | Notes |
|---|---|---|
order_id | Yes | Your own order reference, up to 100 characters. Also your idempotency key — see below. |
amount | Yes | Decimal string, 0.50 to 100000, at most 2 decimals. |
currency | No | USD. It is the only value accepted today. |
description | No | Shown to the customer on the checkout. Up to 255 characters. |
customer_email | No | Stored with the invoice. Ignored if it is not a valid address. |
success_url | No | Must be https://. Falls back to your website setting. |
cancel_url | No | Must be https://. Falls back to your website setting. |
webhook_url | No | Must be https://. Overrides your website setting for this invoice. |
POST https://payavex.com/api/v1/invoice
Content-Type: application/json
{
"order_id": "ORDER-1001",
"amount": "25.00",
"currency": "USD",
"description": "Amazon Gift Card $25",
"customer_email": "buyer@example.com",
"success_url": "https://yourstore.com/order/1001",
"cancel_url": "https://yourstore.com/cart",
"webhook_url": "https://yourstore.com/payavex-callback"
}201 Created
{
"id": "CX7K2M9QX4B1",
"order_id": "ORDER-1001",
"status": "new",
"price_amount": "25.00",
"price_currency": "USD",
"merchant": "Your Store",
"checkout_url": "https://payavex.com/pay/CX7K2M9QX4B1",
"success_url": "https://yourstore.com/order/1001",
"cancel_url": "https://yourstore.com/cart"
}Retrying is safe
Posting the same order_id and the same amount again returns the invoice that already exists, as long as it is still usable, instead of creating a second one. Use your own order reference and a timeout-retry cannot double-charge a customer.
Changing the amount for the same order reference does create a new invoice, because it is a different thing to pay for.
Check the status
GET /api/v1/invoice/{id} — the current state of one of your invoices. This is the authoritative check. A customer landing on your success URL is not proof of payment.
GET https://payavex.com/api/v1/invoice/CX7K2M9QX4B1
# for GET the signed string is the path, not a body:
# hash_hmac('sha256', $ts . '.' . '/api/v1/invoice/CX7K2M9QX4B1', $secret)Once a coin is chosen the response also carries the coin object (symbol, network, confirmations, contract), the crypto pay_amount, the receiving address, any memo, the amount received so far and the list of transfers seen.
Statuses
| Status | Meaning |
|---|---|
new | Created. The customer has not chosen a coin yet. |
waiting | A coin and network are chosen; waiting for the transfer. |
detected | The transfer was seen on-chain and is collecting confirmations. |
paid | Confirmed in full. This is the only status that should fulfil an order. |
underpaid | Less arrived than the invoice asked for. Held for review. |
late | Arrived after the quote expired. Held for review. |
expired | The quote window passed with nothing received. |
cancelled | Cancelled before payment. |
paid only. detected means a transfer was seen, not that it has confirmed.List available coins
GET /api/v1/coins — the assets currently enabled for payment, with each one’s network and minimum order value. Useful if you want to show the list on your own page before sending the customer to checkout.
It is a live list: an asset the operator disables stops appearing here and stops being offered at checkout.
Callbacks
When an invoice changes state we POST the same JSON as the status endpoint to your callback URL, signed the same way the API requests are — X-Timestamp and X-Signature over <timestamp>.<raw body> with your API secret.
Verify the signature before trusting a single field, then reply 200. Any other response is treated as a failure and the callback is retried on a backing-off schedule for up to 24 hours.
<?php
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$ts = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, $API_SECRET);
if (!hash_equals($expected, strtolower($sig))) {
http_response_code(401);
exit;
}
$e = json_decode($raw, true);
if ($e['status'] === 'paid') {
// idempotent: do nothing if this order is already fulfilled
fulfil_order($e['order_id'], $e['id']);
}
http_response_code(200);
echo 'ok';Make your handler repeatable
- The same state can arrive more than once — a retry, or a second confirmation. Check whether the order is already fulfilled before fulfilling it.
- Callbacks can arrive out of order. Treat the payload as the current state, not as an event log, or re-read the status endpoint before acting.
- Compare the
order_idand the amount against your own record before releasing anything. - Reply quickly and do the slow work afterwards. A handler that times out looks like a failure and will be retried.
Embedded checkout
You can open the checkout in a popup on your own page instead of navigating away. Include embed.js and call Payavex.open() with the invoice ID.
<script src="https://payavex.com/embed.js"></script>
<button id="pay">Pay with crypto</button>
<script>
document.getElementById("pay").onclick = function () {
Payavex.open("CX7K2M9QX4B1", {
onPaid: function () { location.reload(); },
onClose: function () { /* tidy up your page URL */ }
});
};
Payavex.autoResume(); // reopens after an app switch on mobile
</script>The popup is an iframe of the checkout page, so it is only allowed on domains registered for your website. Payavex.autoResume() reopens it after a page reload, which is what happens when a phone switches apps mid-payment.
The onPaid callback is a signal to refresh your page — not proof of payment. Confirm on your server.
Errors
Errors return a JSON body with a single error string.
| Status | Meaning | What to do |
|---|---|---|
400 | Body was not valid JSON. | Fix the request body. |
401 | Missing headers, expired timestamp, unknown API key or bad signature. | Check the clock on your server and that you signed the right string. |
404 | No such invoice for this website. | Check the invoice ID. An invoice belongs to one website only. |
422 | A field failed validation — amount out of range, missing order reference, non-HTTPS callback URL. | Read error in the response body; it names the problem. |
500 | Something failed on our side. | Retry with the same order_id; it is safe. |
Keeping it safe
- Your API secret stays on your server. Never in browser JavaScript, a mobile app, a public repository or a support ticket.
- The API key identifies your website; the secret signs requests and verifies callbacks. Both are per-website, and one website’s credentials never work for another.
- Fulfil on a server-side status check or a signature-verified callback. Never on a redirect, a query parameter or a screenshot.
- Compare signatures with a constant-time function —
hash_equalsin PHP,crypto.timingSafeEqualin Node. - Keep your server clock in sync. A drift over 5 minutes fails every request with a 401.
- If a secret is exposed, ask the operator to rotate it in the workspace and deploy the new one. Rotation invalidates the old secret immediately.