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
| Ability | Grants |
|---|---|
| payment-requests:create | Create and cancel payment requests |
| payment-requests:read | Read payment request status |
| transactions:read | Read your wallet transactions |
| wallets:read | List wallets and balances |
| webhooks:receive | Receive 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 redirect | On your domainEmbedded popup | No codeWooCommerce plugin | |
|---|---|---|---|
| Effort to integrate | One API call plus a redirect | API call, a popup, and a message listener | Install plugin, paste API key |
| Buyer stays on your site | No, full-page redirect to Payclio | Yes, popup opens over your page | Yes, popup at checkout |
| Payment methods | Wallet, card, Apple Pay, Google Pay | Wallet and card (same as hosted) | Wallet and card |
| Card data touches your server | Never (SAQ-A) | Never (SAQ-A) | Never (SAQ-A) |
| How payment confirms | return_url plus status poll or webhook | postMessage plus status poll or webhook | Built in: webhook and Action Scheduler poll |
| You write code | A little | A little more (front and back end) | None |
| Best for | Fastest path, any stack | Custom checkouts that must stay on brand | WordPress 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
Create the request
Tie it to your local order via reference_type and reference_id so you can reconcile later.
- 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
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 + cardKeep 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=1 | Embed mode. Strips the full-page chrome so it sits cleanly in a popup or inline layout. Already present on the returned embed_url. |
| card=1 | Optional. 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. |
| name | Optional. Prefills the payer name. URL-encoded. |
| Optional. Prefills the payer email. URL-encoded. | |
| phone | Optional. 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">×</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.event | Meaning |
|---|---|
| payment.ready | The card field has rendered and is interactive. |
| payment.paid | The card was charged. A hint to confirm server-side, not proof of payment. |
| payment.expired | The request passed its expires_at before completion. |
| payment.cancelled | The 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
- On
payment.paid(and on a 5s safety poll while the page is open), call your own confirm endpoint. - That endpoint calls
GET /api/v1/payment-requests/{token}and marks the order paid only whenstatus === 'paid'. Make it idempotent. - The
payment_request.paidwebhook 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.
payment-requests:createRequest body
| amount | String or number, required. Up to 2 decimals. |
| currency | Required. USD, EUR, GBP, NAD, ZAR. |
| memo | Optional. 280 chars. Shown to the payer. |
| reference_type | Optional. e.g. woocommerce_order. Lowercase, hyphens or underscores ok. |
| reference_id | Optional. Your internal order ID. Up to 64 chars. |
| return_url | Optional. We redirect the payer here after settlement. |
| expires_in_minutes | Optional. 1 to 43200 (30 days). Default: no expiry. |
| to_wallet_id | Optional. 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.
payment-requests:readReturns 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
}
}
}payment-requests:createMarks 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.
List your wallets with balances. Ability: wallets:read.
Single-wallet balance. Ability: wallets:read.
Paginated wallet activity. Ability: transactions:read.
Single transaction by UUID. Ability: transactions:read.
Webhooks
LiveAdd 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
| Event | Fires when |
|---|---|
| payment_request.paid | A payer settles a pending request via wallet or card. |
| payment_request.expired | A pending request passes its expires_at. |
| payment_request.cancelled | You 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.
| Status | Name | When you'll see it |
|---|---|---|
| 400 | Bad Request | Malformed JSON or missing Content-Type header on POST. |
| 401 | Unauthenticated | Missing or invalid bearer token. |
| 403 | Forbidden | Token does not carry the ability the endpoint requires. |
| 404 | Not Found | Token does not exist, or belongs to another account. |
| 422 | Validation failed | Field-level validation errors. Body contains errors keyed by field. |
| 429 | Too many requests | Rate limit exceeded, retry after the Retry-After header. |
| 500 | Server error | Something 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.