Using Payday
Hosted checkout
Every deposit_url points at a page Payday hosts. It shows the payer exactly what they need, withholds what they should not see, and offers three ways to pay the same one-time address.
Gated request, before verification
Acme LLC requests
March retainer
Amount
Network
Address
Deadlinein 23h 41m
After verification and the wallet step
Acme LLC requests
March retainer
Amount25.00 USDC
NetworkMonad · native USDC
Address0x8f3C…a1D2
Deadlinein 23h 41m
request.pdf
What the payer sees#
- The request: who is asking, the heading and reference, the payer's name, notes, and the attached PDF through a short-lived link.
- The instructions: the amount still due, the network and the exact token contract, the one-time address, and a QR code encoding the same request.
- The clock: a countdown to the deadline, driven by Payday's clock, never the device's. When it reaches zero the page waits for the chain's verdict rather than declaring the request expired itself.
- Live status: the page polls and moves on its own as finalized transfers arrive, then shows the settlement transaction once the request settles.
For a gated request only the issuer name and heading show until the payer's session satisfies the policy; see Verifying the payer. For every request, the address, the QR, and the pay button appear only after the wallet step.
Three ways to pay#
| Way | How it works |
|---|---|
| Connected wallet | The page discovers installed browser wallets and, with WalletConnect, phone wallets. It checks the chain and the token contract, then sends a plain USDC transfer of the amount still due, re-read at the moment of signing. No approval, no contract call. It refuses to send from any wallet but the attested one. |
| Scanned QR | An EIP-681 request for the amount still due, which a mobile wallet turns into a pre-filled transfer. |
| Copied address | The address in full, for a wallet or exchange withdrawal. The payer must send from the wallet they signed with. |
All three stop being offered the moment the request is no longer payable, because funds sent afterwards route back to the payer rather than to you.
The link#
A deposit link is open by design: anyone holding it may read the request and fulfil it, which is what makes it shareable. It carries no merchant data. Your payout address, the recovery address, metadata, the customer, and the policy assertions never reach the page. Search engines are told not to index it.
When the request names a payer.email, Payday emails that address as the request is issued: a message in Payday's design naming the issuer, the amount, the heading and reference, and the deadline, with a button to the same link. Merchant-session requests are never emailed, since their link opens only from your application.
Building your own checkout#
The hosted page is built on Payday's public payer routes, and so can yours. They take no API key, return no merchant data, and answer reads from any browser origin. The verification and wallet writes are answered for the hosted checkout's origin only, so a custom checkout drives those from a server or asks Payday to admit its origin.
import { PaydayPayerClient } from "@payday/sdk";
const payer = new PaydayPayerClient(); // no key: these routes are public
const request = await payer.depositRequests.get("dr_0198f80c-…");
request.issuer_name; // always present, with heading
request.content_unlocked; // false while a gated request awaits verification
request.requirements; // { email, wallet, merchant_session, complete }
request.remaining_base_units; // the only value to do arithmetic on
request.address; // null until the payer's wallet is bound
request.deposit_uri; // EIP-681 request for the amount still due
request.payable; // false once the address must stop being shown
request.server_timestamp; // render the deadline from this, not the device clockEmail verification#
// The code goes to the mailbox the merchant asserted; the payer only types it.
const { payer_session } = await payer.verification.startEmail(request.id);
const { requirements } = await payer.verification.confirmEmail(request.id, "123456", payer_session);
// requirements.complete is now true; pass payer_session on every later read.The wallet step#
Every request, gated or not, takes this step before it has an address. The typed data comes from Payday and goes to the wallet verbatim.
import { createWalletClient, custom } from "viem";
const wallet = createWalletClient({ transport: custom(window.ethereum) });
const [account] = await wallet.getAddresses();
// 1. Ask Payday for the document to sign. A permissionless request with no
// session yet gets one here; a gated request needs the session that
// satisfied its policy.
const challenge = await payer.wallet.challenge(request.id, account, { payerSession });
// 2. The wallet shows the typed data in full and signs it. No transaction.
const signature = await wallet.signTypedData({ account, ...challenge.typed_data });
// 3. Hand the signature back. The response is the unlocked request with
// `address` and `payer_wallet` set.
const ready = await payer.wallet.attest(request.id, account, signature, challenge.payer_session);Then render address, deposit_uri, and the QR from GET /v1/payer/deposit-requests/{id}/qr, and poll the request until it settles. The routes are listed under Payer routes.