Add crypto payments to your site
1 minute. No backend required.
You get two keys, and the difference matters:
pk_live_… (publishable) — goes in your website's button, below. Public by design: all it can do is create a payment that pays into your wallets.
sk_live_… (secret) — logs into your dashboard and can change your payout wallets. Never put it in a web page, an app, or anything a visitor can view the source of. Server-side only.
data-amount — price in whatever unit you fixed: crypto (0.5 XMR, 0.001 BTC) if data-currency is set, or fiat if data-fiat-currency is set instead
data-currency — XMR, BTC, LTC, ETH, SOL, or ZEC. Fixes the crypto — no picker shown.
data-reference — your order ID (optional)
data-server — your PlatypusPay URL (default: https://platypuspay.xyz)
The amount lives in the page, so a determined visitor can edit it before clicking — that's true of any browser-side button, ours included. Before you ship an order, check the amount you were actually paid: the payment.confirmed webhook in step 05 carries received_amount, and that number comes from the chain, not from the browser. For anything where being underpaid would hurt, create the payment from your server (step 04) instead.
One of data-currency or data-fiat-currency is required — a bare amount with neither is ambiguous (29.99 what?), so the button refuses to run and logs an error to the console instead.
amount — same rule as data-amount: a fixed crypto amount if you set currency, otherwise a fiat amount (requires fiat_currency). Recompute this from the cart on your server — never trust a total sent by the browser.
success_url / cancel_url — where the checkout page sends the customer once payment is confirmed on-chain, or if it expires. Not available as data-attributes on the widget — this is why a real cart needs the API, not just the button.
collect_info: true — adds a name/email/phone/address form to the checkout page before the payment address is shown, saved against the payment and visible in your dashboard. Turn this on if you're shipping something physical.
merchant_reference — anything that helps you match the payment back to an order later: an order ID, or a short summary of what's in the cart.
How long the link lives — set currency and the payment window starts immediately: 30 minutes, 45 for Bitcoin. Omit it (customer picks their coin at checkout) and the link stays valid for a week, with the payment window starting only when they choose. That second form is what to use for a link you email or print on an invoice — no address exists until the customer picks, and your price stays fixed in fiat and converts at the rate of the moment they pay.
Set your Webhook URL under your dashboard → Settings → "Webhook URL". Your signing secret is just below it, under "webhook signing".
PlatypusPay-Signature is t=<timestamp>,v1=<hex> — an HMAC-SHA256 over "{timestamp}.{raw body}" using your webhook secret. Reject the request if the signature doesn't match, or if the timestamp is too old (replay protection) — don't just check the header is present.
Three events exist: payment.confirmed (status: "confirmed") fires once, when the full amount arrives on-chain within the payment window — this is the one to fulfill orders on. payment.partial (status: "partial") fires if a payment expires having received only part of the amount: the partial funds are still forwarded to you, but do NOT treat it as paid in full — check received_amount against expected_amount before shipping anything. payment.late (status: "late") fires when the full amount arrives, but after the window closed — a slow Bitcoin confirmation, an exchange holding a withdrawal, or a swap service freezing a transfer will all do it, and we keep watching for a week. Those funds are forwarded to you like any other, but the order is deliberately not confirmed: the crypto amount was priced when the invoice was created, so a late payment can be worth noticeably less than the order. Decide whether to ship it. Always branch on the event/status field, never assume every webhook means "confirmed". None of them fire on payment creation.
status — filter on one of pending, confirmed, expired, partial, late. Omit it and you get everything.
limit — up to 100, 25 by default. When has_more is true, ask for the next page with starting_after set to the last id of the page you just read. Paging this way stays correct while new payments keep arriving at the top of the list.
requested_amount / received_fiat_amount — what the order was priced at, and what the crypto was worth at the moment it landed. Both are what you want for bookkeeping; they are null on invoices you priced directly in crypto.
DELETE /payments/{id} closes an invoice early — a cart the customer walked away from, an order cancelled by hand. It returns the payment with status: "expired", and returns 409 instead if any money has already arrived on it, since those funds are on their way to your wallet. A closed invoice keeps being watched for a week like any other, so a customer who pays late still reaches you through payment.late rather than disappearing.
GET /merchants/me — your account as it stands: name, email, publishable key, payout addresses, accepted_currencies (the currencies your customers are actually offered at checkout, derived from the addresses you configured), webhook URL. Use PUT /merchants/me with any of the same fields to change them.
All of these take your secret key. The publishable key creates payments and nothing else — it never reads your history.
No website? Every account also gets a hosted donation page — no HTML needed, just a link. There are actually two, same form, different wording: a neutral donation link for anywhere (social bio, website, email signature) under your dashboard → Settings → "Donations", and a stream donation link that tells donors their name is shown live, under Settings → "Stream alerts" — use that one for your Twitch/YouTube bio instead (see step 08).
Your ready-to-paste overlay URL is on your dashboard → Settings → "Stream alerts" (your donation link itself is under "Donations", just above it) — no need to build either URL by hand. Confirmation time depends on the coin — SOL or ZEC for near-instant alerts, BTC if you don't mind a few minutes' delay.
That's it. You're accepting crypto.
Don't have an account yet?
Create free account