Test modeWhitechain Sepolia· WB Soul verification is simulated · test USDT only
tazor

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

  1. The customer clicks Pay with Tazor on the merchant's checkout. The button shows the amount.
  2. The Tazor app opens the invoice. The customer's approvers check it and approve it with a passkey; anyone, including the relayer, submits it.
  3. The button changes to Waiting for payment…, then Paid ✓ · View tx once the payment is on Whitechain, and fires tazor-paid.
  4. 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

AttributeMeaning
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.
merchantWallet mode, required. The address that receives the USDT (and signs refunds).
amountThe USDT amount, e.g. "0.20" (up to 6 decimals). Required in wallet mode.
order-refWallet mode, required. Your order number; it becomes the on-chain order reference.
deadlineWallet mode. Minutes the customer has to pay (default 30).
depositWallet mode. "false" hides "Pay from WhiteBIT or any exchange".
demo-walletWallet 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.
relayerWallet mode. The relayer that submits the signature (default Tazor's).
supplier-nameRequired. Your company name, shown on the invoice.
payeeRequired. The Whitechain address that receives the USDT.
buyerRequired. The customer's Tazor company account; its approvers approve and pay.
amountA single amount in USDT, e.g. 12.50. Use this or lines.
linesJSON array of { description, quantity?, unitPrice } with unitPrice as a USDT string.
memoShown to the customer, e.g. an order or PO number.
dueDue day as YYYY-MM-DD. Defaults to 14 days from today.
invoice-idYour order number. Defaults to INV-YYYYMMDD-XXXX.
tax-rate, tax-labelOptional tax, added as a final line the way the app adds it (label defaults to Tax).
supplier-accountYour Tazor account, if it isn't the payee.
app-urlThe Tazor app that opens the link. Defaults to https://tazor.charlesms.dev.
labelButton text. Defaults to "Pay with Tazor".
targetpopup opens a small window; anything else opens a new tab.
explorerBlockscout 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

EventDetail
tazor-finalityWallet mode: each finality stage of the payment, { orderId, stage: "pending" | "sequencer" | "ethereum" | "finalized" }.
tazor-linkThe 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-paidPaid on Whitechain: { txHash, amount, at, payer, payee, explorerUrl, invoiceHash, url, id }.
tazor-errorBad 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

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

The link carries the invoice encrypted with a key that only travels in the link's # part, so neither the merchant's server nor Tazor sees it. The invoiceHash reveals nothing about the invoice by itself.
This build runs on Whitechain Sepolia with test USDT. The buyer must be a Tazor company account whose approvers have passkeys on the Tazor app.