// overview
lolipop is payment infrastructure for accepting Monero: you create a payment from your server with one API call, the buyer gets a hosted checkout page with a unique address and amount, and when the chain confirms the payment we notify you. We never hold your XMR and we never need your spend key.
The model is view-key based: you paste your wallet's primary address and secret view key into the dashboard. That lets our systems derive per-order subaddresses and recognize incoming transfers. A view key is read-only by design in Monero — it cannot sign spends.
// end-to-end payment flow
- Your backend calls POST /v1/pay with amount, currency, and an order reference.
- lolipop locks an XMR amount (from a cached rate), creates a fresh subaddress for that order, and returns a checkout URL plus the address and expiry window.
- You redirect the buyer to the checkout page (pure HTML, no required JavaScript).
- The buyer sends XMR to the shown address. Our poller matches the transfer to the order via the subaddress.
- After enough confirmations, we mark the order paid and POST a signed webhook to your URL so you can fulfill the order.
Funds move directly on-chain to addresses that belong to your wallet. lolipop does not sit in the middle of the transaction as a custodian.
// why we need your wallet address
The field in settings is your wallet's primary address (sometimes called the main address). It identifies the wallet that should receive funds from orders.
Subaddresses generated for each checkout are still your addresses: they are derived from the same wallet seed as the primary address. You must use the primary address that belongs to the same wallet you export the view key from. If the address and view key come from different wallets, incoming payments will not match what we expect and orders will not confirm correctly.
We never ask for your spend key or seed phrase. Only the primary address and view key are required for the non-custodial flow.
// why we need your secret view key
In Monero, a secret view key lets a wallet (or a service holding only that key) see which outputs belong to your wallet — amounts and transaction links from your perspective — without being able to spend them. The spend key stays on your device; we never see it.
lolipop uses the view key together with wallet tooling (for example monero-wallet-rpc) to:
- create a new subaddress per order;
- watch for incoming payments to that subaddress;
- match payments to the right order and confirmation depth.
Your view key is encrypted at rest on our side. You can replace it anytime by saving a new one in settings; the old material stops being used for new orders.
Export the secret view key from a wallet that supports it — for example Cake Wallet, Feather, or the official Monero GUI. Wording in each app differs slightly; look for "view key" or "secret view key" in export or backup screens.
If our systems were fully compromised, an attacker could learn what the view key already reveals (incoming history for that wallet) but could not move your funds.
// buyer checkout page
Checkout is server-rendered HTML: QR code (for example inline SVG), address text, amount, and optional product image as an inline data URI where configured. There is no merchant JavaScript on the page, which keeps Tor Browser "Safest" mode and strict no-JS environments viable.
Status can refresh via a simple meta refresh or equivalent so the buyer sees when the payment is detected — without relying on a client-side framework.
// buyer doesn't have XMR?
The checkout page includes a collapsed "don't have XMR yet?" section with Cake Wallet (mobile, built-in swap) and links to no-KYC swap services. This is shown to buyers who need to acquire XMR first.
As a merchant, you can optionally reference these services in your own store copy. lolipop is not affiliated with any swap service and does not earn fees from swaps.
A full guide for buyers lives at /get-xmr — you can link to it from your own store or checkout copy. The checkout page also includes the collapsed "don't have XMR yet?" section with links to no-KYC swap services and wallets.
// webhooks
A webhook URL is an https:// endpoint on your server. When an order reaches the required confirmation depth, lolipop sends an HTTP POST with a JSON body describing the payment so you can update your database, send email, unlock digital goods, or trigger any server-side logic.
Configure the URL in dashboard settings. Use a dedicated path (for example /webhooks/lolipop) and reject requests that are not correctly signed (see below).
// webhook signing & verification
Set a webhook secret in settings. We use it to compute an HMAC over the raw request body. Your application must verify the signature before trusting the payload.
Check the X-Lolipop-Signature header (prefix sha256=, then the hex digest) using HMAC-SHA256 with your secret and the raw POST body. Reject mismatches, replays outside an allowed time window, and duplicate delivery IDs if you track them.
// notifications
The email field is used for account-related communication (such as magic-link login). Keep it accurate so you do not miss sign-in messages. Subscription invoices and due dates for lolipop itself live in the dashboard (see platform billing below); we do not email those invoices in this version.
XMPP
Optional Jabber/XMPP address for future or experimental merchant alerts. Same privacy posture as email: we only use it to reach you, not buyers.
In Dashboard → Settings → Alerts you can turn outbound billing and API-security emails/XMPP on or off. Critical auth alerts (e.g. TOTP lockout) may still be sent regardless.
// API & secret key
Server-to-server calls use a secret API key you receive when you first verify the magic link. Send it in the Authorization: Bearer sk_live_... header. Prefer that only your own backends hold the key — except for the optional public donate widget, which uses a dedicated key with allowed HTTPS origins instead of an IP list (mutually exclusive on the same key). The widget supports light and dark UI (system preference by default; see appearance).
You can create multiple keys with separate labels, scoped permissions (create_orders, read_orders, create_links, webhook_test), and either an optional IP allowlist or allowed origins from the dashboard: settings → API keys. Keys are stored with bcrypt; older accounts may show a one-time migration to the new model. Prefer narrow permissions and rotation on a schedule or after any suspected leak.
Free plan: POST /v1/pay is disabled. Create checkouts from the dashboard using payment links (session auth, same JSON body). Paid plans can use the API from any backend.
// POST /v1/pay
Base URL is your lolipop API host (e.g. https://api.lolipop.cash). Path: /v1/pay. Method: POST. Content-Type: application/json.
Browser calls: preflight OPTIONS /v1/pay with Origin and X-Lolipop-Key-Prefix (first 16 chars of the secret). POST responses echo CORS Origin when the key has allowed origins and the request origin matches. See /docs/widget.
Request body (fields):
amount — amount (number) — required for single-amount checkouts; ignored when items is present (total is derived from line items)currency — currency (string, optional) — invoice fiat for quoting and checkout labels; default USD. Supported: USD, EUR, GBP, CHF, CAD, AUD, JPY, CZK, PLN, SEK, NOK, DKKref — ref (string, required) — your opaque order id / correlation idname — name (string, optional) — label shown on checkout (invoice title when itemized)image_url — image_url (string, optional) — product image; fetched server-side and inlined on checkout (single-amount flow)items — items (array, optional) — line items per row with fields name, quantity, and amount (amount_usd still accepted as a legacy alias). quantity must be a whole number ≥ 1. Total in invoice currency is the sum of quantity × amount per rowexpires_in — expires_in (number, optional) — seconds until checkout expiry; default 1800; clamped between 60 and 86400webhook_url — webhook_url (string, optional) — override webhook for this order only (HMAC still uses your merchant webhook secret)
Success response 200 JSON:
url — url — redirect the buyer hereaddress — address — XMR subaddress for this orderamount_xmr — amount_xmr — locked XMR amount (string, up to 8 decimals)currency — currency — invoice fiat (ISO code) used for the quoteamount — amount — total in that fiat (matches your request or derived from line items)amount_usd — amount_usd — included only when currency is USD (backward compatible echo)expires_in — expires_in — seconds until checkout window ends (typically 1800)items — items — when the order is itemized, the same line items you sent
Errors return JSON with an error string and optional code: 400 validation, 401 bad API key, 403 free plan blocked from POST /v1/pay or missing key permission / IP allowlist, 429 with monthly_order_limit when checkout creation exceeds your plan's per-month cap (UTC calendar month; applies to API and payment links), 503 wallet unavailable.
POST /v1/pay
Authorization: Bearer sk_live_...
Content-Type: application/json
{
"amount": 49.99,
"currency": "EUR",
"ref": "order_123",
"name": "1yr VPN Plan"
}
// POST /api/merchant/payment-links
Dashboard session (HttpOnly cookie after magic link) or a Bearer API key that has the create_links permission. Other dashboard routes (settings, billing, webhooks list, …) require session — API keys cannot manage themselves. Same request body as POST /v1/pay, except ref is optional — a link_… ref is generated if omitted. Response shape matches /v1/pay, including optional items when you created an itemized invoice.
GET /api/merchant/orders (session or API key) returns amount, currency, and amount_usd only for USD orders, plus optional items when itemized.
POST /api/merchant/payment-links
Cookie: session=... # or
Authorization: Bearer sk_live_...
Content-Type: application/json
{
"items": [
{ "name": "Sticker pack", "quantity": 2, "amount": 5 },
{ "name": "Shipping", "quantity": 1, "amount": 3.5 }
],
"name": "Order #1042",
"currency": "EUR",
"expires_in": 1800,
"webhook_url": "https://example.com/hooks/lolipop"
}
// GET /v1/orders/{token}/status
Public polling endpoint (no auth). token is the short id from the checkout path /c/{token}. Returns status, amount_xmr, amount, currency, amount_usd when the invoice is USD, address, expires_at, and when paid paid_at / txid.
// Webhook payload
On sufficient confirmations we POST JSON to your webhook URL. Headers: Content-Type: application/json, X-Lolipop-Event: payment.confirmed, X-Lolipop-Signature: sha256= followed by the hex digest (HMAC-SHA256 of the raw body with your webhook secret).
payment.confirmed body fields include event, order_id, ref, amount_xmr, amount, currency, amount_usd only for USD invoices, txid, paid_at, and when the checkout had line items, items (array of name / quantity / amount). Test deliveries use event: webhook.test from the dashboard.
// WooCommerce (WordPress)
The lolipop Pay gateway is a PHP plugin for WooCommerce: server-side only, hosted Monero checkout, same webhooks as a custom integration. Full install and field-by-field setup lives on the dedicated page — WooCommerce setup guide. Plugin ZIPs are published on GitLab releases.
// integration examples
Call POST /v1/pay from your backend (PHP, Python, Go, Node, etc.) with your secret key — request and response fields are in POST /v1/pay above and in the OpenAPI document. Verify webhooks with HMAC-SHA256 using your signing secret; headers and payload shape are under Webhook signing and Webhook payload above. For a browser donate button, use /docs/widget (widget.js).
// rotating your API key
Open settings → API keys to rotate or revoke individual keys. Rotation issues a new secret and keeps the old key valid for 24 hours (grace), then it stops working — update staging and production env vars before the grace window ends. Revoke disables a key immediately.
// deleting your account
Deleting your merchant account removes lolipop-side configuration: API access, webhook settings, stored view key material, and related records according to our retention policy. It does not reverse blockchain transactions; funds already received remain in your wallet.
// more
Product FAQ and positioning live on the FAQ page. Terms and privacy are linked from the site footer. API, webhooks, and the embed widget are covered in the sections above; see also integration examples.