Skip to main content
The tally.payments resource is where money moves. See Payments (concept) for the two-layer enforcement model and statuses.

Types

Payment

The fields below the comment are populated on responses from 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.
Parameters All fields are validated server-side. 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):
  1. 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 throw ValidationError with structured error.code.
  2. 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.
Throws

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.
Returns: 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.
Pending outbound rows are lazily refreshed from the chain on read — polling 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:
Two 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 to payment.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.
Tracked in BUILD_LOG.md.