Payments, exports, and withdrawals
Payment submission, received-payment reporting, and withdrawals are different operations. Creating an invoice does not pay it. A withdrawal moves existing Safe funds and requires a valid Safe-authorized signature.
Public payer endpoints#
These routes do not require HMAC or a Privy JWT. They use the deployment's default chain. Metadata access requires the invoice's access token; transaction submission requires payer signatures. See the pay-page guide.
| Endpoint | Request | Success response |
|---|---|---|
GET /v1/pay/:id | Numeric invoice ID | { success, data: invoice, metadata } |
GET /v1/pay/:id/metadata | Optional token=clr_tk_... query | { success, accessLevel, data, metadata } |
GET /v1/pay/:id/pdf | Valid invoice token query is required | PDF attachment; missing token returns 400 TOKEN_REQUIRED |
POST /v1/pay/:id/permit | { payerAddress, permitDeadline? } | { success, permitToSign, invoice } |
POST /v1/pay/:id/prepare | { payerAddress, ownerAddress, permit: { deadline, v, r, s } } | { success, userOp, userOpHash, permitDeadline, expiresAt, invoice } |
POST /v1/pay/:id | { userOp, permitSignature?: { v, r, s } } | { status: "confirmed", userOpHash, txHash }, or 202 with status: "pending", userOpHash, and message |
payerAddress in this flow is the payer's Safe (it sends the UserOp and owns the permit), and ownerAddress is its owner EOA. Sign the permit first: /permit returns the EIP-2612 permit for the Safe (permitDeadline is Unix seconds, defaulting to one hour, at most 24 hours), the Safe signs it, and /prepare embeds that signature in the UserOp callData, checks it on-chain (ECDSA or ERC-1271) and only then requests paymaster sponsorship. Sign the returned UserOp exactly as returned; userOpHash is its canonical EntryPoint v0.7 hash. Submit it unchanged: the server forwards the same bytes to the bundler and never rebuilds callData. An HMAC signature cannot substitute for a permit or UserOp signature. These endpoints describe the Safe-based flow, not a generic adapter for every external wallet. For normal customer payments, use the tokenized pay link and its supported wallet flow.
A permit with less than two minutes left is rejected with 409 PERMIT_EXPIRED at prepare or submit; request a new permit and prepare again. 400 PERMIT_SIGNATURE_INVALID means the permit signature does not verify for payerAddress. 400 USEROP_PERMIT_MISMATCH means the submitted UserOp carries no signed permit, or permitSignature differs from the one in its callData. permitSignature is optional and only cross-checked.
Prepared payment and withdrawal expiresAt values are ISO date-time strings, unlike Unix-second permit deadlines; a prepared payment never expires after its permit. Permit r and s are 32-byte hex strings, and v is a number from 0 through 255.
Public reads return raw-unit amount and on-chain status. The fee fields distinguish current previews from history: effectiveFeeBps is the merchant's current override for an Issued invoice, where zero means the retail schedule, not zero fees. settledFee and settledFeeBps describe a paid invoice when settlement-event data is available. A null fee value means unavailable or not applicable, not necessarily zero. See fees.
Without a metadata token, GET /metadata returns accessLevel: "onchain_only" and metadata: null. A valid token allows decrypted billing metadata. Invalid, expired, or revoked tokens do not grant that access. Preserve ?token=... in share links and avoid logging it.
Received payments#
GET /v1/payments accepts HMAC or JWT, with studio ownership filtering and no explicit granular scope guard. It lists paid invoices in reverse chronological payment order.
| Query | Constraint |
|---|---|
paidFrom, paidTo | Optional integer Unix seconds; paidTo must not precede paidFrom |
payer | Optional EVM address |
merchant | Accepted by schema, but listing always uses the caller's Safe |
limit | Integer 1 through 50, default 25 |
cursor | Returned pagination cursor |
Response: { data: { payments, page, totals }, metadata }. Each payment includes id, invoiceNumber, merchant, payer, amount, paidAt, grossAmount, protocolFee, netAmount, transactionHash, explorerUrl, token, tokenURI, and issuedAt when available. Amount and fee fields are raw USDC integer strings. totals.totalAmount, totals.totalFees, and totals.count summarize the current page, not the entire account.
If settlement-event details are missing, the current response falls back to invoice amount for gross/net and "0" for the protocol fee; the transaction hash may be null. Do not interpret this fallback as proof of a fee waiver. Reconcile settlement evidence before final accounting.
Export payments#
GET /v1/payments/export requires HMAC or JWT. It accepts:
format: required,csvorjson.startDate,endDate: both required, integer Unix seconds, with end not before start.payer: optional address.merchant: optional address; if supplied it must match your studio's Safe or the request returns403.
Example read-only path for October 2026 in UTC:
/v1/payments/export?format=json&startDate=1790812800&endDate=1793491199Sign this exact query string for HMAC. CSV returns an attachment. JSON returns { exportedAt, dateRange, summary, payments }, without a data wrapper. Summary fields are totalPayments, totalAmount, totalFees, totalNetAmount, and currency. Payment rows use date, invoiceNumber, invoiceId, amount, fee, netAmount, payerAddress, transactionHash, and explorerUrl.
Export amounts are human-readable USDC strings rounded to two decimals, unlike raw-unit list responses. The formatter uses floating-point conversion, so exports are not an exact arbitrary-precision round trip for sub-cent or very large amounts. Retain raw-unit payment data for exact accounting. Exports render invoice numbers differently from dashboard labels; join records by numeric invoiceId, not formatted text. The server fetches all matching pages in memory, so select bounded date ranges for large histories. See export data.
Withdraw funds#
HTTP routes accept HMAC or JWT but have no explicit granular scope guard. That does not grant an API key the owner's signing authority. The invoice server signer cannot sign USDC withdrawals. Prefer the withdraw-funds dashboard flow unless you have implemented owner-controlled Safe signing.
| Endpoint | Request | Response |
|---|---|---|
POST /v1/withdrawals/prepare | { ownerAddress, amount, destination } | { success, userOp, userOpHash, expiresAt, withdrawalSummary } |
POST /v1/withdrawals | { userOp, amount, destination } | Confirmed: { status: "confirmed", withdrawalId, userOpHash, txHash, explorerUrl }; pending: 202 { status: "pending", withdrawalId, userOpHash, message } |
GET /v1/withdrawals | limit 1 through 50 (default 20), offset nonnegative integer (default 0) | { success, data: { items, total, limit, offset } } |
Before preparation, the owner must set the withdrawal address. destination must match it and must differ from the Safe. amount is a positive decimal integer string in USDC base units. Preparation checks funds and builds a sponsored operation. The client reviews and signs it with valid Safe owner authority before submission. Submit the same amount/destination with the signed UserOp. Never paste the API secret into a wallet-signature field.
Withdrawal history rows contain id, raw amount, display amountFormatted, destination, status, txHash, userOpHash, optional explorerUrl, and date-time createdAt/confirmedAt. A timeout is not proof that funds stayed in the Safe. Persist hashes and reconcile the original operation before creating another withdrawal; invoice idempotency support does not imply withdrawal idempotency support.