# Payment request

> Invoice privately: issue a request, get paid against it, and have the payment match itself to the invoice.

A recipient generates a payment request and sends the link to the payer. The payer sends the funds and the recipient then discovers the incoming payment via the request's tag.

## Why use it

-   **Neither party reveals their address.** Two people can request and settle a payment without learning each other's wallet address. The shared request carries per-request keys and pre-computed note slots rather than the recipient's address, and the payer fulfills it through a relayer, so neither side sees the other's wallet.
-   **The recipient doesn't need to be registered.** Receiving normally relies on a [viewing key](/concepts/keys) registered in the [keystore](/protocol/contracts/keystore); a request instead supplies its own per-request key, so even a brand-new wallet can be paid before it ever registers.
-   **Payments reconcile to your own records.** Issue one request per invoice or order and keep the request-to-reference mapping locally. When a payment arrives you can match it back to that invoice, even though the on-chain note carries no invoice number.

## Step 1: recipient creates a request

```ts
const toHex = (value: bigint): `0x${string}` => `0x${value.toString(16)}`;
const paymentRequest = await session.generatePaymentRequest({
    amount: toHex(parseUnits("0.5", 6)),
    tokenId: USDC_SEPOLIA,
});

console.log("Pay this:", JSON.stringify(paymentRequest));
```

`generatePaymentRequest()` returns the public request shape: `paymentId`, `noteAddressHashes`, `amount`, `tokenId`, `tag`, and `paymentPubKey`. Recipient secrets stay in local request storage. Your app decides how to encode the public request into a link or QR code. If your app also packages recipient recovery state such as `noteSecretsMap`, treat that payload as sensitive.

## Step 2: payer sends

```ts
const prepared = await session.prepareFulfillPaymentRequest({
    paymentRequest: paymentRequestFromShareLink,
    inputCommitments: [/* picked input */],
    // optional: relayAddress, feeAmount, processorAddress to override
});

await session.relayTransfer(prepared.relayOptions[0]);
```

`prepareFulfillPaymentRequest()` wraps `prepareTransfer()` with the request's tag and encryption target. It returns the same relay/self-submit option shape as a normal transfer.

## Step 3: recipient discovers

```ts
await session.discoverNotes();

const acct = await session.exportAccount();
const matched = acct.notes.filter(
    (n) => paymentRequest.noteAddressHashes.includes(n.noteAddressHash),
);

for (const note of matched) {
    console.log("Received:", note.value);
}
```

Discovery re-derives each open request's `paymentPrivKey`, matches `Note` events by tag, and marks matched requests as fulfilled. Notes do not carry `paymentId`, so correlate them with the request's pre-committed `noteAddressHashes`.

## Gotchas

-   **Tag isn't authenticated:** a sender can stamp a payment with a different request's tag. Pair with off-chain signing if non-repudiation matters.
-   **One event batch, many tags:** discovery reuses the same [ASP](/concepts/asp-attestation)/RPC Note event batch and matches events against your open request tags. Prune fulfilled requests to keep the map small.
