API v1, live

Accept payments anywhere with one API call.

Mint a payment request, redirect (or iframe) your customer to a Payclio-hosted page, and we settle in your wallet. Wallet, card, Apple Pay, and Google Pay are supported on the hosted side, 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 a bearer token in the Authorization header. Generate keys from your dashboard once account API keys are available; each key is bound to a set of abilities (scopes).

Authorization: Bearer 17|pDblEyXo9tUvaJfN...
Accept: application/json
Content-Type: application/json

Token abilities

AbilityGrants
payment-requests:createCreate and cancel payment requests
payment-requests:readRead payment request status
transactions:readRead your wallet transactions
wallets:readList wallets and balances
webhooks:receiveReceive HMAC-signed webhooks at your configured endpoints

Failing a scope check returns 403 with no body. Always mint a key with the minimum abilities needed.

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, card, Apple Pay, Google PayWallet and card (same as hosted)Wallet and card
Card data touches your serverNever (SAQ-A)Never (SAQ-A)Never (SAQ-A)
How payment confirmsreturn_url plus status poll or webhookpostMessage plus status poll or webhookBuilt 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 handles wallet, card, Apple Pay, and Google Pay, 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 payment, Payclio redirects to your return_url with ?payclio_token=...&payclio_status=paid. Verify by calling GET /payment-requests/{token} server-side before marking the order paid. In popup mode, listen for window.message events with { source: 'payclio', event: 'payment.paid' }.

Embedded checkout

Wallet + card

Keep the buyer on your own domain. Instead of redirecting to the hosted page, render the embed_urlfrom 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://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.
payment.expiredThe request passed its expires_at before completion.
payment.cancelledThe payer abandoned or cancelled.
// 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
    case 'payment.expired':   showExpired();                       break;
    case 'payment.cancelled': showCancelled();                     break;
  }
});

// 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-requestsAbility: payment-requests:create

Request body

amountString or number, required. Up to 2 decimals.
currencyRequired. USD, EUR, GBP, NAD, ZAR.
memoOptional. 280 chars. Shown to the payer.
reference_typeOptional. e.g. woocommerce_order. Lowercase, hyphens or underscores ok.
reference_idOptional. Your internal order ID. Up to 64 chars.
return_urlOptional. We redirect the payer here after settlement.
expires_in_minutesOptional. 1 to 43200 (30 days). Default: no expiry.
to_wallet_idOptional. Pick a specific receiving wallet. Defaults to your active {currency} wallet.

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 201 Created

{
  "message": "Payment request created.",
  "data": {
    "payment_request": {
      "token": "a1b2c3d4e5f6g7h8",
      "uuid": "9b1a...",
      "status": "pending",
      "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/",
      "hosted_url": "https://payclio.com/req/a1b2c3d4e5f6g7h8",
      "embed_url":  "https://payclio.com/req/a1b2c3d4e5f6g7h8?embed=1",
      "qr_svg_data_uri": "data:image/svg+xml;base64,...",
      "expires_at": "2026-05-13T14:00:00+00:00",
      "created_at": "2026-05-13T13:00:00+00:00"
    }
  }
}

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

GET/api/v1/payment-requests/{token}Ability: payment-requests:read

Returns the current state of a payment request. Lazy-expires on read: if past expires_at and still pending, status flips to expired. Use this on your settlement callback to verify before marking an order paid.

{
  "data": {
    "payment_request": {
      "token": "a1b2c3d4e5f6g7h8",
      "status": "paid",
      "amount": "49.99",
      "currency": "USD",
      "paid_at": "2026-05-13T13:08:42+00:00",
      "transaction_id": 4821,
      "paid_by_user_id": 9
    }
  }
}
POST/api/v1/payment-requests/{token}/cancelAbility: payment-requests:create

Marks a pending request cancelled. Idempotency: a second call returns 422 with a clear message. Cannot cancel a paid request, issue a refund through your dashboard instead.

Other endpoints

Used less often than payment requests but useful for richer integrations.

GET
/api/v1/wallets

List your wallets with balances. Ability: wallets:read.

GET
/api/v1/wallets/{id}/balance

Single-wallet balance. Ability: wallets:read.

GET
/api/v1/transactions

Paginated wallet activity. Ability: transactions:read.

GET
/api/v1/transactions/{uuid}

Single transaction by UUID. Ability: transactions:read.

Webhooks

Live

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

Events

EventFires when
payment_request.paidA payer settles a pending request via wallet or card.
payment_request.expiredA pending request passes its expires_at.
payment_request.cancelledYou call POST /payment-requests/{token}/cancel.

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:00+00:00",
  "data": {
    "token": "a1b2c3d4e5f6g7h8",
    "status": "paid",
    "amount": "49.99",
    "currency": "USD",
    "reference_type": "woocommerce_order",
    "reference_id": "1234",
    "paid_at": "2026-05-13T13:59:55+00:00",
    "transaction_id": 4821
  }
}

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

Every error response is JSON with a top-level message field. Validation failures additionally include an errors object keyed by field.

StatusNameWhen you'll see it
400Bad RequestMalformed JSON or missing Content-Type header on POST.
401UnauthenticatedMissing or invalid bearer token.
403ForbiddenToken does not carry the ability the endpoint requires.
404Not FoundToken does not exist, or belongs to another account.
422Validation failedField-level validation errors. Body contains errors keyed by field.
429Too many requestsRate limit exceeded, retry after the Retry-After header.
500Server errorSomething on our end. Safe to retry.

Already on WooCommerce?

Skip the integration work, install our WooCommerce plugin. Popup checkout with wallet and card, full return_url wiring, Action Scheduler fallback polling. Generate an API key with payment-requests:create and payment-requests:read, drop the .zip into Plugins, Add New, Upload, paste the key, save.

đź›’