Connect your checkout.

Payment APIs for connected stores.

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.

Everything on this page is the live contract of this gateway. There is no separate sandbox: to try it end to end, use a real invoice for a small amount, or ask the operator to create a preview invoice for your website from the workspace.

Authentication

Every request carries three headers. There is no bearer token and no session.

HeaderValue
X-Api-KeyYour website’s API key.
X-TimestampCurrent Unix time in seconds. Rejected if more than 5 minutes from our clock.
X-SignatureLowercase 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: $sig

Create a payment

POST /api/v1/invoice — returns the invoice and its checkout_url.

FieldRequiredNotes
order_idYesYour own order reference, up to 100 characters. Also your idempotency key — see below.
amountYesDecimal string, 0.50 to 100000, at most 2 decimals.
currencyNoUSD. It is the only value accepted today.
descriptionNoShown to the customer on the checkout. Up to 255 characters.
customer_emailNoStored with the invoice. Ignored if it is not a valid address.
success_urlNoMust be https://. Falls back to your website setting.
cancel_urlNoMust be https://. Falls back to your website setting.
webhook_urlNoMust be https://. Overrides your website setting for this invoice.
Request
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"
}
Response
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

StatusMeaning
newCreated. The customer has not chosen a coin yet.
waitingA coin and network are chosen; waiting for the transfer.
detectedThe transfer was seen on-chain and is collecting confirmations.
paidConfirmed in full. This is the only status that should fulfil an order.
underpaidLess arrived than the invoice asked for. Held for review.
lateArrived after the quote expired. Held for review.
expiredThe quote window passed with nothing received.
cancelledCancelled before payment.
Fulfil an order on 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_id and 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.

StatusMeaningWhat to do
400Body was not valid JSON.Fix the request body.
401Missing headers, expired timestamp, unknown API key or bad signature.Check the clock on your server and that you signed the right string.
404No such invoice for this website.Check the invoice ID. An invoice belongs to one website only.
422A field failed validation — amount out of range, missing order reference, non-HTTPS callback URL.Read error in the response body; it names the problem.
500Something 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_equals in PHP, crypto.timingSafeEqual in 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.
Never handle seed phrases or private keys. Payavex never asks for them and neither should your integration. Nothing in this API requires a customer’s wallet keys.

Credentials live in the workspace.

Your API key, signing secret and callback URL are managed by the operator.