Embed quickstart · 5 min · zero backend

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.

Plain HTML Vanilla JS React Next.js

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.

Bundle
~30 kb gzipped
Settlement
<2 s SOL · ~12 s ETH · ~10 min BTC
Protocol fee
0% — only the network's own gas
1

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.

zettapay.io · onboarding
CREATE A MERCHANT Get your API key No credit card. No KYC. Pubkey lives in your code — you'll set it as an env var next. Merchant handle @yourshop you@yourshop.com Continue → Free tier · 100 txs / month
Free, no protocol fee. ZettaPay is a non-custodial protocol — we never touch the funds. The customer pays the network's own gas (a fraction of a cent on Solana, a few cents on Ethereum L2s, a few sats on Bitcoin). ZettaPay charges nothing on top.
2

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.

No on-chain bind required. The protocol simply watches the addresses you list. You keep full control of the wallet, can sweep funds whenever you want, and can swap addresses mid-flight without touching the ZettaPay dashboard.
3

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.

zettapay.io/dashboard · Install
DASHBOARD › @yourshop LIVE · DEVNET EMBED CODE Drop this into your <head> Amount (USDC) 10 <script src= "https://cdn.jsdelivr.net/npm/@zettapay/widget@latest/dist/widget.js" data-merchant= "@yourshop" data-amount= "10" async ></script> COPIED ✓

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>
4

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.

~/yourshop/public/index.html — VS Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <!doctype html> <html lang = "en" > <head> <meta charset = "utf-8" /> <title> Yourshop </title> <!-- ZettaPay --> <script src = "https://cdn.jsdelivr.net/npm/@zettapay/widget@latest/dist/widget.js" data-merchant = "@yourshop" data-amount = "10" async ></script> </head> <body> <!-- your site --> PASTED
That is the integration. No npm install, no build step, no backend, no API key. The CDN bundle is ~30 kb gzipped, served from jsDelivr's global edge. Reload your site — a Pay button is there.
5

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.

yourshop.com · checkout success
Payment received + 10.00 USDC Tx 5h2k…fJ8z CONFIRMED · 1.8 SEC · SOLANA

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.

Compare: Stripe takes 2-3 days to settle to your bank and disputes ride your account for weeks. ZettaPay settles directly on-chain in seconds, charges no protocol fee on top of network gas, and the transaction is final — there is no party in the middle to dispute with.

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).

preview · iframe sandbox

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>
SSR safe. The bundle no-ops during server render and mounts on hydration. Next.js, Remix, SvelteKit, Astro, Nuxt — all work without flags.

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.

What's next