Pay button SDK
Take a Tazor payment on any website with one script tag, one element and one event.
@tazor/pay lets another website take a Tazor payment. It makes the same encrypted payment link the Invoices page makes, opens it in the Tazor app, and watches Whitechain until the invoice is paid. It has no server and no API key, and its only dependency is viem.
What the merchant and the customer see
- The customer clicks Pay with Tazor on the merchant's checkout. The button shows the amount.
- The Tazor app opens the invoice. The customer's approvers check it and approve it with a passkey; anyone, including the relayer, submits it.
- The button changes to Waiting for payment…, then Paid ✓ · View tx once the payment is on Whitechain, and fires
tazor-paid. - The merchant's own page or dashboard can watch the same invoice with
watchPayment, on any device.
Install
It isn't on npm yet. In the repository, build it from packages/pay with pnpm build, which writes dist/index.js (ES module) and dist/tazor-pay.js (one script for a <script> tag). The merchant demo in examples/merchant-demo already has a copy.
The whole integration: 15 lines
This is the integration from the Bluejay Logistics demo: one script, one element, one listener. There is no build step and no framework. In mode="wallet" the customer pays from any wallet holding USDT with one signature (no gas, no WBT; Tazor's relayer submits it), or by a plain transfer from WhiteBIT or any exchange.
<script src="./tazor-pay.js"></script>
<tazor-pay-button
mode="wallet" demo-wallet="true"
merchant="0xc1dce946D192e344251972470237F1471C93876c"
amount="0.20"
order-ref="BJ-1042"
label="Pay 0.20 USDT"></tazor-pay-button>
<script>
document.querySelector('tazor-pay-button').addEventListener('tazor-paid', (e) => {
document.querySelector('#order-status').textContent = 'Paid';
const tx = document.querySelector('#order-tx');
tx.href = e.detail.explorerUrl;
tx.hidden = false;
});
</script>From code
import { createInvoiceLink, watchPayment } from '@tazor/pay';
const link = await createInvoiceLink({
supplierName: 'Bluejay Logistics',
payee: '0xc1dce946D192e344251972470237F1471C93876c',
buyerAccount: '0xb99A12652BEe98429ae31EA0529A0b63f1073721',
lines: [{ description: 'Freight', quantity: 1, unitPrice: '100' }],
taxRate: 18, taxLabel: 'GST',
dueDate: '2026-12-31',
id: 'BJ-1042',
});
// link.url → https://tazor.charlesms.dev/pay/#v1.… link.amount → '118'
const stop = watchPayment({
invoiceHash: link.invoiceHash,
payee: '0xc1dce946D192e344251972470237F1471C93876c',
onPaid: ({ txHash, amount, at }) => markOrderPaid('BJ-1042', txHash),
});<tazor-pay-button> attributes
| Attribute | Meaning |
|---|---|
mode | "wallet" for wallet checkout (one EIP-3009 signature, no gas) with a "Pay from WhiteBIT or any exchange" option; leave it out for Tazor company invoices. |
merchant | Wallet mode, required. The address that receives the USDT (and signs refunds). |
amount | The USDT amount, e.g. "0.20" (up to 6 decimals). Required in wallet mode. |
order-ref | Wallet mode, required. Your order number; it becomes the on-chain order reference. |
deadline | Wallet mode. Minutes the customer has to pay (default 30). |
deposit | Wallet mode. "false" hides "Pay from WhiteBIT or any exchange". |
demo-wallet | Wallet mode, testnet demos only. "true" adds "Pay with a demo wallet (no extension)": a wallet made in the page and topped up with test USDT by the sandbox. |
relayer | Wallet mode. The relayer that submits the signature (default Tazor's). |
supplier-name | Required. Your company name, shown on the invoice. |
payee | Required. The Whitechain address that receives the USDT. |
buyer | Required. The customer's Tazor company account; its approvers approve and pay. |
amount | A single amount in USDT, e.g. 12.50. Use this or lines. |
lines | JSON array of { description, quantity?, unitPrice } with unitPrice as a USDT string. |
memo | Shown to the customer, e.g. an order or PO number. |
due | Due day as YYYY-MM-DD. Defaults to 14 days from today. |
invoice-id | Your order number. Defaults to INV-YYYYMMDD-XXXX. |
tax-rate, tax-label | Optional tax, added as a final line the way the app adds it (label defaults to Tax). |
supplier-account | Your Tazor account, if it isn't the payee. |
app-url | The Tazor app that opens the link. Defaults to https://tazor.charlesms.dev. |
label | Button text. Defaults to "Pay with Tazor". |
target | popup opens a small window; anything else opens a new tab. |
explorer | Blockscout base URL. Defaults to the Whitechain testnet explorer. |
Style it with CSS variables (--tazor-bg, --tazor-fg, --tazor-accent, --tazor-radius, --tazor-font, --tazor-paid) or with the parts ::part(button), ::part(label), ::part(status) and ::part(tx).
Events
| Event | Detail |
|---|---|
tazor-finality | Wallet mode: each finality stage of the payment, { orderId, stage: "pending" | "sequencer" | "ethereum" | "finalized" }. |
tazor-link | The payment link is ready: { id, url, invoiceHash, amount, invoice }. element.link is the same value as a promise, if you need it before your listener exists. |
tazor-paid | Paid on Whitechain: { txHash, amount, at, payer, payee, explorerUrl, invoiceHash, url, id }. |
tazor-error | Bad attributes or a failed link: { message }. |
The events bubble and cross the shadow root, so you can listen on the element or on document.
API
| Function | Returns |
|---|---|
payOrder({ order, wallet }) | Signs the order in any wallet (walletSigner(window.ethereum) or accountSigner(viemAccount)) and has the relayer submit it. Also refundOrder, subscribe, cancelSubscription, chargeSubscription, getOrder, watchOrder. |
depositOrder({ merchant, amount, ref, refundTo }) | The per-order address to send USDT to by plain transfer, read from TazorDepositCheckout; watchDeposit(order, onUpdate) follows it. |
verifyWebhook(rawBody, header, secret) | Checks the Tazor-Signature header (t=…,v1=HMAC-SHA256) within ±300 s and returns the event. |
payFetch(url, { account, maxValue }) | For agents: pays an HTTP 402 (x402 exact, USDT on Whitechain) once, refusing anything above maxValue. |
createInvoiceLink(input) | Promise of { id, url, invoiceHash, amount, amountBaseUnits, invoice }. Input: supplierName, payee, buyerAccount, lines, dueDate (YYYY-MM-DD, Date or unix seconds), and optional supplierAccount, taxRate, taxLabel, memo, id, appUrl. |
watchPayment({ invoiceHash, onPaid, payee?, intervalMs?, explorer?, onError? }) | Polls Whitechain every 4 s by default and calls onPaid({ txHash, amount, at, payer, payee, viaL1, explorerUrl }) once. Returns stop(). |
isPaid(invoiceHash, opts?) | Promise of true once the invoice is paid. |
getPayment(invoiceHash, opts?) | The payment, or null. |
decodeInvoiceLink(url) | The invoice inside a link, exactly as the app decrypts it. |
defineTazorPayButton(tag?) | Registers the custom element. tazor-pay.js already does this. |
How payment is detected
PaymentModule emits InvoicePaid(account, nonce, payee, amount, viaL1), and the nonce is the invoice hash (keccak256 of the canonical invoice). The SDK reads the newest logs of PaymentModule v1 and v2 from the Whitechain Blockscout API and matches that nonce, and the payee if you pass it. Payments sent through Ethereum count too.
# part, so neither the merchant's server nor Tazor sees it. The invoiceHash reveals nothing about the invoice by itself.buyer must be a Tazor company account whose approvers have passkeys on the Tazor app.