API v1, live

Accept payments anywhere with one API call.

Create a payment request, send your customer to the Payclio-hosted page (or open it in a popup), and the money goes to your wallet. The hosted page takes wallet and card payments, so your server never touches card data.

# 1. Create a payment request
curl https://payclio.com/api/v1/payment-requests \
  -H "Authorization: Bearer $PAYCLIO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":"49.99","currency":"USD",
       "memo":"Order #1234"}'

# 2. Redirect customer to the returned hosted_url
# 3. Poll status until paid:
curl https://payclio.com/api/v1/payment-requests/$TOKEN \
  -H "Authorization: Bearer $PAYCLIO_KEY"

Authentication

Every request carries your API key as a bearer token in the Authorization header. Create keys under API keys in your business account dashboard and revoke them at any time. A key acts for the whole business account, so keep it on your server. Creating payment requests needs a business account whose ID check is approved.

Authorization: Bearer <your API key>
Accept: application/json
Content-Type: application/json

Choose your integration

Three ways to take a payment, same payment-request engine underneath. Pick by how much control you need over the checkout UI versus how little code you want to write.

SimplestHosted redirectOn your domainEmbedded popupNo codeWooCommerce plugin
Effort to integrateOne API call plus a redirectAPI call, a popup, and a message listenerInstall plugin, paste API key
Buyer stays on your siteNo, full-page redirect to PayclioYes, popup opens over your pageYes, popup at checkout
Payment methodsWallet and cardWallet and card (same as hosted)Wallet and card
Card data touches your serverNever (SAQ-A)Never (SAQ-A)Never (SAQ-A)
How payment confirmsWebhook or status pollpostMessage plus webhook or status pollBuilt in: webhook and Action Scheduler poll
You write codeA littleA little more (front and back end)None
Best forFastest path, any stackCustom checkouts that must stay on brandWordPress and WooCommerce stores

Not sure? Start with hosted redirect, it is the fastest to ship and you can move to the embedded iframe later without changing how you create or settle requests.

Quickstart, accept a card payment

The hosted-redirect flow is the most common integration. You hand off the customer to a Payclio-hosted page that takes wallet and card payments, keeping your PCI scope at SAQ-A.

  1. 1

    Create the request

    Tie it to your local order via reference_type and reference_id so you can reconcile later.

  2. 2

    Redirect (or iframe) to the hosted page

    Send the customer to the returned hosted_url for a full-page redirect, or render embed_url in an iframe to keep them on your site.

  3. 3

    Settle

    After paying, the customer sees a confirmation and is sent to your return_url after 3 seconds, with ?payclio_token=...&payclio_status=paid added. That redirect is not proof of payment: mark the order paid when the payment_request.paid webhook arrives, or when GET /payment-requests/{token} returns status paid. In popup mode there is no redirect, listen for window.message events with { source: 'payclio', event: 'payment.paid' } instead.

Embedded checkout

Wallet + card

Keep the buyer on your own domain. Instead of redirecting to the hosted page, render the embed_url from a payment request in an iframe inside a popup, with the payer's details prefilled. Both wallet and card show by default, same as a direct visit to the hosted page, buyer picks. Your server never touches card data, the iframe is served by Payclio and stays at SAQ-A scope.

The browser postMessage event is a UX trigger, not proof of payment. Always confirm status === 'paid' from your server via GET /payment-requests/{token} (or a verified webhook) before releasing goods.

Wallet payment needs a real signed-in Payclio session. Browsers increasingly block third-party cookies inside a cross-site iframe (Safari ITP, Chrome's phase-out), so when embedded, choosing wallet opens the same link in a new tab rather than trying to authenticate inside the frame, this is the hosted page's own behaviour, nothing you need to build. Card needs no session at all and always stays inline.

1. Build the popup

A button that opens a modal on click, not an always-visible iframe, so nothing loads (or sits blank) until the buyer actually wants to pay. Lazy-load the iframe src from the embed_url returned by POST /payment-requests (it already carries ?embed=1) plus the prefill params below, and show a loading state until the frame's load event fires. The allow="payment *" attribute is required for 3-D Secure inside the frame.

Query params

embed=1Embed mode. Strips the full-page chrome so it sits cleanly in a popup or inline layout. Already present on the returned embed_url.
card=1Optional. Card-only: hides the wallet tab, for a guest checkout where no one has a Payclio wallet to sign into. Omit to show both, our own recommendation.
nameOptional. Prefills the payer name. URL-encoded.
emailOptional. Prefills the payer email. URL-encoded.
phoneOptional. Prefills the payer phone in E.164. URL-encoded.
<!-- A button that opens the checkout in a popup, not an always-visible
     iframe: nothing loads (or shows a blank white box) until the buyer
     actually asks to pay. Wallet and card both appear, buyer picks. -->
<button id="payclio-open" type="button">Pay securely with Payclio</button>

<div id="payclio-modal" style="display:none;position:fixed;inset:0;z-index:100;
     align-items:center;justify-content:center;">
  <div id="payclio-backdrop" style="position:absolute;inset:0;background:rgba(0,0,0,.45)"></div>
  <div style="position:relative;background:#fff;width:100%;max-width:480px;
       max-height:92vh;border-radius:16px;overflow:hidden;display:flex;flex-direction:column;">
    <div style="display:flex;justify-content:space-between;align-items:center;
         padding:14px 18px;border-bottom:1px solid #f1f5f9;">
      <strong>Pay securely with Payclio</strong>
      <button id="payclio-close" type="button" aria-label="Close">&times;</button>
    </div>
    <div style="position:relative;flex:1;overflow-y:auto">
      <div id="payclio-loading" style="position:absolute;inset:0;display:flex;
           align-items:center;justify-content:center;background:#fff;">Loading…</div>
      <iframe
        id="payclio-iframe"
        data-src="https://app.payclio.com/req/<token>?embed=1&name=Jane%20Doe&email=jane%40acme.com&phone=%2B264811234567"
        allow="payment *; clipboard-write"
        referrerpolicy="strict-origin"
        style="width:100%;min-height:560px;border:0;opacity:0;transition:opacity .2s"
        title="Secure payment">
      </iframe>
    </div>
  </div>
</div>

<script>
  var open_   = document.getElementById('payclio-open');
  var modal   = document.getElementById('payclio-modal');
  var iframe  = document.getElementById('payclio-iframe');
  var loading = document.getElementById('payclio-loading');

  open_.addEventListener('click', function () {
    modal.style.display = 'flex';
    if (!iframe.src) iframe.src = iframe.dataset.src; // lazy-load on first open
  });
  document.getElementById('payclio-close').addEventListener('click', close);
  document.getElementById('payclio-backdrop').addEventListener('click', close);
  function close() { modal.style.display = 'none'; }

  iframe.addEventListener('load', function () {
    loading.style.display = 'none';
    iframe.style.opacity = '1';
  });
</script>

Vanilla HTML/JS, adapt the styling to your own design system. Prefilled fields save the buyer re-typing what they entered at checkout.

2. Listen for events, then confirm server-side

The frame posts messages to the parent window. Trust only messages whose origin matches the embed origin and whose data.source === 'payclio'.

data.eventMeaning
payment.readyThe card field has rendered and is interactive.
payment.paidThe card was charged. A hint to confirm server-side, not proof of payment.
// Trust only messages from the embed origin AND source: 'payclio'.
const origin = new URL(embedUrl).origin;

window.addEventListener('message', (e) => {
  if (e.origin !== origin) return;
  const d = e.data || {};
  if (d.source !== 'payclio') return;

  switch (d.event) {
    case 'payment.ready': /* card field rendered */ break;
    case 'payment.paid':  confirmOnServer();        break; // hint only
  }
});

// Authoritative: never fulfil on the browser event alone. Have YOUR server
// re-check status and only then mark the order paid (idempotently).
async function confirmOnServer() {
  await fetch('/checkout/payclio/confirm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token }),
  });
  // server-side: GET /api/v1/payment-requests/{token} -> mark paid iff status === 'paid'
}

Recommended settlement

  1. On payment.paid (and on a 5s safety poll while the page is open), call your own confirm endpoint.
  2. That endpoint calls GET /api/v1/payment-requests/{token} and marks the order paid only when status === 'paid'. Make it idempotent.
  3. The payment_request.paid webhook may settle the same order a moment earlier, both paths are safe to run together.

Prefer the embed but want a no-code option? The WooCommerce plugin ships this exact flow.

Payment Requests

Each payment request is a one-shot, amount-locked checkout. After it's paid (or expired, or cancelled), it cannot be reused, mint a fresh one for the next order.

POST/api/v1/payment-requests

Request body

amountRequired. Number or numeric string, 0.01 to 1,000,000.
currencyRequired. USD, EUR, GBP, NAD or ZAR. Paid into your wallet in that currency, created for you if you do not have it yet.
memoOptional. 280 chars. Shown to the payer.
reference_typeOptional. Your own label, e.g. woocommerce_order.
reference_idOptional. Your internal order ID.
return_urlOptional. Full http or https address. After paying on the hosted page (not in popup mode) the customer is sent here with payclio_token and payclio_status=paid added.
expires_in_minutesOptional. 1 to 43200 (30 days). Default 1440 (24 hours).

Example

{
  "amount": "49.99",
  "currency": "USD",
  "memo": "Order #1234, Acme Store",
  "reference_type": "woocommerce_order",
  "reference_id": "1234",
  "return_url": "https://example.com/checkout/order-received/1234/",
  "expires_in_minutes": 60
}

Response 200 OK

{
  "data": {
    "payment_request": {
      "token": "a1b2c3d4e5f6g7h8",
      "uuid": "9b1a...",
      "status": "pending",
      "amount": 49.99,
      "currency": "USD",
      "memo": "Order #1234, Acme Store",
      "hosted_url": "https://app.payclio.com/req/a1b2c3d4e5f6g7h8",
      "embed_url": "https://app.payclio.com/req/a1b2c3d4e5f6g7h8?embed=1",
      "reference_type": "woocommerce_order",
      "reference_id": "1234",
      "return_url": "https://example.com/checkout/order-received/1234/",
      "transaction_id": null,
      "created_at": "2026-05-13T13:00:00Z",
      "expires_at": "2026-05-13T14:00:00Z",
      "paid_at": null
    }
  }
}

Redirect the customer to hosted_url, or render embed_url in an iframe.

GET/api/v1/payment-requests/{token}

Returns the current state of a payment request. If it is past expires_at and still pending, status reads expired. Status is one of pending, paid, expired or cancelled. Use this to verify before marking an order paid.

{
  "data": {
    "payment_request": {
      "token": "a1b2c3d4e5f6g7h8",
      "status": "paid",
      "amount": 49.99,
      "currency": "USD",
      "transaction_id": 4821,
      "paid_at": "2026-05-13T13:08:42Z",
      ...
    }
  }
}

Webhooks

Live

Add endpoints under Webhooks in your dashboard. We POST a signed JSON payload when a payment request is paid, expires or is cancelled. Retries on non-2xx and timeouts: 30s, 5m, 30m, 2h, 12h.

Events

EventFires when
payment_request.paidA payer pays a pending request by wallet or card.
payment_request.expiredSomeone tries to pay a request after its expires_at. Do not wait for it to time out an order, check expires_at yourself.
payment_request.cancelledYou cancel the request in your dashboard.

Payload

POST https://your-store.com/payclio/webhook
Content-Type: application/json
X-Payclio-Event: payment_request.paid
X-Payclio-Event-Id: evt_a1b2c3d4e5f6g7h8i9j0
X-Payclio-Signature: t=1715600000,v1=8e3a...4f1c

{
  "id": "evt_a1b2c3d4e5f6g7h8i9j0",
  "event": "payment_request.paid",
  "created_at": "2026-05-13T14:00:00Z",
  "data": {
    "token": "a1b2c3d4e5f6g7h8",
    "uuid": "9b1a...",
    "status": "paid",
    "amount": "49.99",
    "currency": "USD",
    "memo": "Order #1234, Acme Store",
    "reference_type": "woocommerce_order",
    "reference_id": "1234",
    "return_url": "https://example.com/checkout/order-received/1234/",
    "paid_at": "2026-05-13T13:59:55Z",
    "paid_by_user_id": 9,
    "transaction_id": 4821,
    "expires_at": "2026-05-13T14:00:00Z",
    "created_at": "2026-05-13T13:00:00Z"
  }
}

Verifying signatures

// PHP: recompute and constant-time compare
$body   = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_PAYCLIO_SIGNATURE'] ?? '';
preg_match('/t=(\d+),v1=([0-9a-f]+)/', $header, $m);
[$_, $ts, $sig] = $m;

$expected = hash_hmac('sha256', "$ts.$body", $secret);
if (! hash_equals($expected, $sig)) {
    http_response_code(400); exit;
}

Reject events older than about 5 minutes (replay protection). Respond 2xx within 10 seconds, non-2xx and timeouts trigger the retry schedule above.

Errors

Error responses from the API are JSON like {"error": "Amount must be between 0.01 and 1,000,000.", "code": "VALIDATION_ERROR"}. Show or log the error text, it says what to fix.

StatusNameWhen you'll see it
400Bad RequestInvalid body or the request can't be done, e.g. amount out of range, or the account is not a verified business account. The error field says why.
401UnauthorizedMissing, invalid or revoked API key.
404Not FoundThe payment request token does not exist, or belongs to another account.
500Server errorSomething went wrong on our side. Safe to retry.

Already on WooCommerce?

Skip the integration work, install our WooCommerce plugin. Popup checkout with wallet and card, webhook handling and Action Scheduler fallback polling. Create an API key in your dashboard, drop the .zip into Plugins, Add New, Upload, paste the key, save.