Invoices API
Invoices have a numeric on-chain id and a separate merchant-sequential invoiceNumber. Use id in all URL paths. Amounts are USDC base-unit strings; invoice timestamps are Unix seconds. Status is Issued, Paid, or Canceled.
For dashboard workflows, see issue an invoice, manage invoices, and share invoices.
Read invoices#
| Endpoint | Input | Success envelope |
|---|---|---|
GET /v1/invoices | List query below | { data: { items, page }, metadata } |
GET /v1/invoices/lookup | Sequential number and issuance year | { data: { id, invoiceNumber } } |
GET /v1/invoices/:id | Numeric ID; optional mode | { data: invoice, metadata } |
GET /v1/invoices/:id/pdf | Numeric ID | PDF bytes, not JSON |
These routes accept HMAC or JWT and restrict private reads to the caller's studio. They currently have no explicit granular scope guard. See the authorization matrix.
List query parameters:
| Parameter | Meaning |
|---|---|
status | Case-sensitive Issued, Paid, or Canceled; omit for all |
customerId | Customer UUID |
payer | Payer EVM address |
issuedFrom, issuedTo | Inclusive issuance-date bounds in integer Unix seconds; reversed ranges are rejected |
limit | Integer 1 through 50, default 25; strong mode clamps to 25 |
cursor | Cursor returned in data.page.cursor; do not synthesize it |
orderBy, orderDirection | Indexer ordering field and asc or desc; defaults are issuedBlock and desc |
mode | eventual, hybrid (default), or strong |
Although the query schema accepts merchant and agent, listing always uses the caller's Safe as merchant and does not forward an agent filter. Do not use those parameters to select another studio or claim an agent-specific list. Follow data.page.hasNextPage and its cursor; lists do not use a page or pageSize parameter.
Invoice cursors represent consumed row offsets, independent of invoice IDs. Keep filters and ordering unchanged between pages. Invalid or obsolete cursors return 400; restart without a cursor. Lists are not snapshots: concurrent changes and indexer lag can affect traversal. During chain-only fallback, payer/customer filtering and indexer ordering are not fully preserved, and some historical timestamps are synthesized rather than settlement-event timestamps. Verify historical accounting against known IDs and transaction evidence.
To resolve INV-2026-000007, request /v1/invoices/lookup?number=7&year=2026. Lookup is scoped to the authenticated studio's Safe, uses the invoice's issuance year, and fetches at most two candidates to detect ambiguity. It returns 404 for no match and 409 for ambiguous data. The returned global id can differ from number; use that id for detail, share, and cancellation routes.
An invoice includes id, invoiceNumber, merchant, agent, payer, token, amount, status, metaHash, tokenURI, timestamps, and customer display fields when available. A payer may be absent before payment. Detail reads can also include emailDelivery. tokenURI alone is not a decrypted invoice or a substitute for a tokenized share link.
Server-signed issuance#
POST /v1/invoices/issue creates an on-chain invoice. It requires HMAC, invoice:write, an active write-enabled key, and an active server signer on the studio's chain. The studio must have a deployed Safe and a complete, valid business profile. Supply an existing customer UUID from that studio. Although customerId is optional in the initial request schema, the issuance workflow requires it.
| Body field | Meaning |
|---|---|
customerId | Required existing customer UUID |
amount | Positive amount in USDC base units, preferably a decimal string |
dueAt | Absolute nonnegative Unix-second timestamp, preferably a decimal string; use exactly one of dueAt and dueDays |
dueDays | Relative nonnegative whole days, anchored to the original idempotency record; use instead of dueAt |
description | Optional string, maximum 500 characters |
sendEmail | Optional boolean; email is opt-in |
Example mutation: issue an invoice#
The following request creates an invoice, and is not a connectivity test. Use a test studio and its actual customer UUID. Sign this body exactly as described in authentication.
POST /v1/invoices/issue
Content-Type: application/json
Idempotency-Key: billing-example-order-1042{
"customerId": "123e4567-e89b-42d3-a456-426614174000",
"amount": "500000000",
"description": "Consulting services",
"dueAt": "1793664000",
"sendEmail": false
}This amount is 500 USDC. The fixed example date is 2026-11-03 at 00:00 UTC. Choose the intended due date before the first attempt, then persist the full payload and idempotency key. On a retry, do not recompute dueAt from the current time.
Alternatively, send "dueDays": 30 instead of dueAt. The server fingerprints the relative term and derives the effective due date from the durable operation's original creation time. Reusing the same key and intent preserves that date across process restarts. Sending both fields, or neither, returns 400.
A normal confirmed 200 response is top-level, not nested under data:
{
"id": "42",
"invoiceNumber": "7",
"amount": "500000000",
"status": "Issued",
"issuedAt": "1791072000",
"dueAt": "1793664000"
}This illustrative subset omits returned userOpHash, txHash, shareLink, paymentUrl, optional delivery, and idempotency fields. Preserve the complete server-provided payment URL, including ?token=....
202 with status: "pending" and userOpHash means a shareable invoice result is not ready. A successful operation without a recoverable invoice event stays pending. A confirmed retry recovers the actual ID, finalizes encrypted metadata, and returns a tokenized payment link. Finalization errors remain retryable with the same key. Do not invent an ID or build /pay/undefined. See errors and retries.
Client-signed issuance#
Use this flow only when your client implements Safe UserOperation construction/signing correctly. HMAC or JWT authenticates HTTP; neither replaces the required transaction signature. The dashboard implements this flow for users.
| Endpoint | Request | Response |
|---|---|---|
POST /v1/invoices/prepare | ownerAddress, raw-unit amount, Unix-second dueAt and issuedAt, tokenURI, 32-byte metaHash and mandateHash; optional quoteId, customerId, metadata | { success, userOp, encryption?, idempotency? } |
POST /v1/invoices | { userOp, pendingInvoiceId?, sendEmail?, customerId? } | Normally { status: "confirmed", userOpHash, txHash, invoiceId, delivery?, idempotency? }, or 202 pending |
Pass full invoice metadata to prepare for the encrypted-metadata workflow. It includes version: "1.0", invoiceNumber, ISO date strings issuedAt/dueAt, merchant and payer billing parties, token details (address, symbol, decimals, chainId), human-readable amount, raw amountRaw, at least one line item, createdBy, and createdAt. A line item has description, positive quantity, unitPrice, and amount, with optional tax fields. Optional metadata includes description, notes, paymentTerms, purchaseOrder, externalRef, quoteId, deliverables, and legalText. These metadata dates differ from the Unix-second prepare fields.
Preparation's encryption object includes pendingInvoiceId, ipfsCid, metaHash, and shareLink. Preserve encryption.pendingInvoiceId as the submit body's pendingInvoiceId so encryption records can be finalized against the confirmed invoice ID. sendEmail: true requires customerId. Preserve final paymaster and gas fields before signing; changing them after signing invalidates the signature. Issuance prepare/submit supports Idempotency-Key; retain the original operation context rather than treating each phase as a new invoice. The client-signed response calls its ID invoiceId, whereas /issue normally returns id.
A serialized v0.7 userOp contains sender, nonce, callData, callGasLimit, verificationGasLimit, preVerificationGas, maxFeePerGas, maxPriorityFeePerGas, and signature, plus returned factory/factoryData and paymaster fields when applicable. Numeric fields are strings in JSON. A placeholder signature is not a signed operation.
Cancel, email, and PDF#
| Endpoint | Request and effect | Result |
|---|---|---|
POST /v1/invoices/:id/cancel/submit | HMAC invoice:write, active signer; send {}. Server signs cancellation. | { id, status: "Canceled", userOpHash, txHash }; may return 202 pending |
POST /v1/invoices/:id/cancel/prepare | HMAC or JWT; { ownerAddress }. Prepares a client-signed cancellation. | { success, userOp, expiresAt }, expiry is an ISO date-time |
POST /v1/invoices/:id/cancel | HMAC or JWT; { userOp } with valid signature. Submits cancellation. | { status: "confirmed", userOpHash, txHash }, or 202 pending |
POST /v1/invoices/:id/send-email | HMAC invoice:write or JWT; { customerId? }. Sends/retries eligible delivery for an Issued invoice. | { delivery } |
GET /v1/invoices/:id/pdf | Authenticated merchant read | application/pdf attachment |
Only Issued invoices can be canceled. Cancellation is not a refund; a paid invoice is terminal. Read the status after a pending cancellation before attempting another write.
The server-signed cancel route tracks attempts internally per studio, chain, and invoice using a fixed cancellation key. It can repoll an existing attempt, but checks current invoice status before that tracking step: an already-canceled invoice can return INVALID_STATUS instead of replaying success. This differs from caller-supplied issuance Idempotency-Key behavior.
Invoice issuance and email delivery are separate outcomes. An invoice can exist even when email is disabled, deferred, or failed. Detail reads expose delivery state when available. For explicit email recovery, provide the matching customer UUID; the route checks it against the invoice's customer hash. A successful issuance retry is not a promise to resend email.
Share links and metadata access tokens#
These merchant-owned routes accept HMAC or JWT. They currently lack explicit granular scope guards, including the mutations. They apply to invoices with encrypted metadata.
| Endpoint | Input | Success response |
|---|---|---|
GET /v1/invoices/:id/share | Numeric ID | { success, invoiceId, hasEncryption, shareLink, tokenHint } |
GET /v1/invoices/:id/tokens | Numeric ID | { success, tokens }, no token secrets |
POST /v1/invoices/:id/tokens | { label?, expiresAt? }, expiry is a date-time string | 201 { success, tokenId, token, shareLink, hint, expiresAt } |
DELETE /v1/invoices/:id/tokens/:tokenId | Token UUID | 200 { success, message } |
POST /v1/invoices/:id/share/regenerate | Numeric ID, no configuration body required | { success, tokenId, token, shareLink, hint } |
Token creation grants access to decrypted metadata. The secret is shown only at creation. Regeneration replaces the default share token and revokes the old default; it does not revoke every separately created token. Revocation is permanent. Treat links as bearer credentials and do not log or publish them unintentionally.
The share response's invoiceId may be an encryption-storage identifier. Continue using the original numeric route ID for invoice API requests. Never remove the query token from shareLink, or reconstruct a bare URL in place of the returned link. See security.