Install a Pay USDC button in five minutes.
Get your API key, configure your pubkey in your own env var
(MERCHANT_SOL_PUBKEY, etc.), copy one
<script> tag, paste it in your site's
<head>. That is the entire integration.
ZettaPay watches the chain for the deposit and posts a webhook the moment it confirms. The
protocol never custodies — the on-chain transfer lands directly in your wallet. No protocol fee
on top of network gas. No servers required.
Overview
The @zettapay/widget bundle is a self-mounting
<script> that renders a Pay button anywhere
you drop the tag. Click → modal opens → payer scans a Solana Pay QR or copies the merchant
address into their own wallet → USDC lands in your wallet. The widget never asks to connect a
wallet; it just renders the QR and watches the chain. You handle nothing.
Get your API key
Open zettapay.io/signup in any modern
browser. The form takes a merchant handle and an email — that is the whole intake. No
credit-card form, no KYC, and we do not ask for your pubkey here.
You'll receive an api_key plus a
webhook_secret.
Set your pubkey in an env var
Drop the public address of any wallet you control — Bitcoin, Ethereum (mainnet or any
L2), Solana for USDC — into the matching env var in your own
.env. Hardware wallets, exchange withdrawal
addresses, mobile wallets all work. ZettaPay never asks to connect, and
your pubkey never touches our database. Add more chains later by adding more env vars and
redeploying — no re-onboarding.
# .env — your servers, your secrets, your pubkeys ZETTAPAY_API_KEY=sk_live_... ZETTAPAY_WEBHOOK_SECRET=whsec_... # Wallet addresses you control — set any subset MERCHANT_BTC_PUBKEY=bc1qx5...e92 MERCHANT_ETH_PUBKEY=0x7a3...4F2 MERCHANT_SOL_PUBKEY=7Np41oeY...YVhT
The widget reads MERCHANT_SOL_PUBKEY (or its
BTC / ETH siblings) at boot from a tiny server endpoint on your side — see step 4 for the
data-pubkey wiring if you'd rather inline it. You
can rotate any of these keys by editing the env file and redeploying; the listener picks up the
new address on the next zp.register() call.
Copy the embed code
You land on your dashboard. The Install tab shows a snippet pre-filled with your handle. Set the amount you want to charge, hit Copy.
The exact snippet you'll see (replace @yourshop
with your real handle):
<!-- ZettaPay · drop into your <head> --> <script src="https://cdn.jsdelivr.net/npm/@zettapay/widget@latest/dist/widget.js" data-merchant="@yourshop" data-amount="10" data-currency="USDC" async ></script>
Paste in your <head>
Open your site's index.html (or layout file —
see framework recipes below).
Paste the snippet anywhere inside <head>.
That's the whole integration.
Receive USDC
A customer clicks the button. The widget opens a modal with a QR code and the merchant address for the chain they pick. They pay from whatever wallet they already use (Phantom, Solflare, MetaMask, Rainbow, a Bitcoin hardware wallet, an exchange withdrawal — anything). ZettaPay watches the chain for the deposit; Solana confirms in <2 seconds, Ethereum in ~12 seconds, Bitcoin in ~10 minutes (1 confirmation). Funds land directly in your wallet — no escrow, no holding period, no manual settlement.
On the merchant side, the dashboard updates in real time — the new payment streams in via the
merchant socket. Your USDC balance reflects
immediately, no batch reconciliation. If you registered a webhook, it fires
payment.confirmed with the on-chain tx
signature attached.
Live preview
Below is the actual widget, embedded with the same script tag from step 3. Click the button — the real ZettaPay modal opens (devnet mode, no funds move).
Sandboxed iframe · the preview hits the public CDN. If your network blocks
cdn.jsdelivr.net, the fallback button appears instead.
Framework recipes
The widget runs anywhere a <script> tag
runs. Pick your stack — every recipe produces the same Pay button.
<!-- public/index.html · paste anywhere inside <head> --> <script src="https://cdn.jsdelivr.net/npm/@zettapay/widget@latest/dist/widget.js" data-merchant="@yourshop" data-amount="24.99" data-label="Buy Pro plan" async ></script>
// vanilla JS — programmatic mount, full control import { mount, open } from '@zettapay/widget'; // Render a Pay button into a specific element mount(document.querySelector('#checkout'), { merchantId: '@yourshop', amount: 24.99, currency: 'USDC', onSuccess: ({ paymentId, txSignature }) => { console.log('paid', paymentId, txSignature); window.location.href = '/thanks'; }, onCancel: ({ reason }) => console.log('cancelled', reason), }); // or open the modal imperatively (no button at all) open({ merchantId: '@yourshop', amount: 5 });
// React — useEffect mounts on render, cleans up on unmount import { useEffect, useRef } from 'react'; import { mount } from '@zettapay/widget'; export function PayButton({ amount }: { amount: number }) { const ref = useRef<HTMLDivElement>(null); useEffect(() => { if (!ref.current) return; const handle = mount(ref.current, { merchantId: '@yourshop', amount, onSuccess: ({ txSignature }) => analytics.track('paid', { txSignature }), }); return () => handle.destroy(); }, [amount]); return <div ref={ref} />; }
// app/layout.tsx · App Router import Script from 'next/script'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> <Script src="https://cdn.jsdelivr.net/npm/@zettapay/widget@latest/dist/widget.js" strategy="afterInteractive" data-merchant="@yourshop" data-amount="24.99" /> </head> <body>{children}</body> </html> ); } // or inside a Client Component for per-page amounts: // 'use client'; import { mount } from '@zettapay/widget';
Options reference
| Attribute | Required | Default | Description |
|---|---|---|---|
data-merchant |
yes | — | Merchant handle, e.g. @yourshop |
data-amount |
yes | — | Amount in data-currency units |
data-currency |
no | USDC |
ISO currency code |
data-label |
no | Pay {amount} {currency} |
Button label override |
data-theme |
no | dark |
dark or light |
data-api-base |
no | https://api.zettapay.io |
Override for staging or self-host |
data-checkout-base |
no | https://pay.zettapay.io |
Hosted checkout origin used in the QR |
data-metadata |
no | — | JSON object persisted on the payment |
For programmatic control (callbacks, dynamic amounts, post-mount destroy), import
{ mount, open } from
@zettapay/widget instead — see the React and
Vanilla recipes above.