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

Verify to view

Amount

Network

Address

Deadlinein 23h 41m

Locked

After verification and the wallet step

Acme LLC requests

March retainer

Verified

Amount25.00 USDC

NetworkMonad · native USDC

Address0x8f3C…a1D2

Deadlinein 23h 41m

request.pdf

25.00 USDCPay now
The same request before and after its payer verifies and signs. A locked page renders nothing it was not sent: the withheld fields are absent from the HTML, not hidden by it.

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#

WayHow it works
Connected walletThe 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 QRAn EIP-681 request for the amount still due, which a mobile wallet turns into a pre-filled transfer.
Copied addressThe 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.

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.

Reading a request
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 clock

Email verification#

TypeScript
// 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.

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