tally.payments resource is where money moves. See Payments (concept) for the two-layer enforcement model and statuses.
Types
Payment
list() (which includes inbound payments). create() and get() only describe outbound payments so they emit the slim shape.
PaymentListFilters
PaymentCreateInput
Methods
payments.create(input)
Signs and broadcasts a USDC transfer. Returns once Privy accepts the signed RPC — does not wait for chain confirmation.
amount_usdc accepts up to 6 decimal places (USDC’s native precision); higher precision is rejected.
Returns: Promise<Payment> with status: "pending" and a populated tx_hash.
Enforcement layers (in order):
- Tally pre-check. Validates the agent has an active grant on
wallet, that the policy allows this payment (per-tx max, recipient/contract allowlist, expiry, daily cap), and that the API key is in the right mode. Failures throwValidationErrorwith structurederror.code. - Privy enclave. Independently re-checks the per-tx max and allowlists when signing. Failures bubble up as
status: "failed"on the resulting Payment — there’s no record-less rejection at this layer.
payments.get(id)
Fetches the current state of a payment by id. If still pending on Tally’s side, this call lazily refreshes from the chain — so polling get is the canonical way to wait for confirmed or failed.
Promise<Payment> reflecting the latest known state. If the chain has confirmed since the last call, status will already be flipped.
Throws: NotFoundError, AuthenticationError.
payments.list(filters?)
Returns an auto-paginating list of payments. The result is an AsyncResourcePage<Payment> — iterate with for await, or call .toArray(n) for a bounded read. Each row carries the rich fields (direction, wallet_address, agent_id, etc.) the slim create() response omits.
list({ status: "pending" }) is a valid way to wait for confirmations without webhooks.
Returns: AsyncResourcePage<Payment>.
Idempotency
idempotency_key makes retries safe across network failures:
create() calls with the same idempotency_key within the same (account, mode) return the original Payment without resubmitting on-chain. The payload is not revalidated — if your second call passes a different to or amount_usdc, the server still returns the original payment as written the first time. Make sure your key derivation is tied to the payload (e.g. include the invoice id in the key), not just the operation.
Use deterministic keys derived from your own data — invoice IDs, request IDs, anything stable. Avoid timestamps or fresh UUIDs; those defeat the purpose by making every retry a fresh request.
Waiting for confirmation
create() returns pending. The chain confirms a few seconds later. Two ways to wait:
Poll
payments.get() lazily refreshes from the chain when called on a pending payment, so polling it is the right shape. Don’t poll faster than every 2 seconds; you’ll burn rate-limit budget without seeing a meaningfully fresher status.
Subscribe to webhooks
For production, prefer webhooks. Subscribe topayment.confirmed and payment.failed and use the SDK’s verifier — see Webhooks.
Error codes
payments.create() can fail with one of these structured err.code values. The first column tells you which SDK exception class wraps the response — branch on instanceof first, then on err.code.
The five 403/forbidden codes are policy violations, not auth failures — surface them in your UI as “this permission doesn’t allow that,” not “your credentials are bad.” The SDK wraps them as
AuthenticationError because that’s what 403 maps to in the class hierarchy; the semantic distinction lives in err.code.
Not yet in the SDK
payments.list({ filters? })— cursor-paginated list with status/agent/direction filters. Currently dashboard-only.payments.refund(id)— first-class refund flow. Today, send a fresh payment in the reverse direction.