The payment object
| Field | Type | Description |
|---|---|---|
id | string | Tally’s internal payment id, used as the path param on GET /v1/payments/{id}. |
status | "pending" | "confirmed" | "failed" | pending until the chain confirms. |
tx_hash | string | null | On-chain transaction hash. Set as soon as Privy accepts the signed RPC; null only on rejected pre-checks (which don’t produce a payment record). |
amount_usdc | string | Decimal USDC amount. Always a string to preserve precision. |
to | string | Recipient address. |
from | string | Sender wallet address. |
memo | string | null | Optional caller-provided memo. |
idempotency_key | string | null | The key the payment was created with, if any. |
created_at | ISO 8601 string | Record creation time. |
POST /v1/payments
Sign and broadcast a USDC transfer.
Request
| Body field | Type | Required | Description |
|---|---|---|---|
agent_id | string (1–64) | Yes | The agent’s public id. Must have an active grant on wallet. |
wallet | string (EVM address) | Yes | Sender wallet, owned by this account, matching the key’s mode. |
to | string (EVM address) | Yes | Recipient. |
amount_usdc | string | Yes | Positive decimal, up to 6 decimal places (USDC’s native precision). |
memo | string (≤200) | No | Stored alongside the transaction. |
idempotency_key | string (1–64) | No | See Idempotency. |
Response
201 Created:
GET /v1/payments/{id} or subscribe to payment.confirmed webhooks.
Enforcement layers
In order, before the response is built:- Tally pre-check. The agent must exist in this (account, mode); the wallet must belong to the account; an active grant must exist; the policy (per-tx max, daily cap, recipient/contract allowlist, expiry) must permit the payment.
- Privy enclave. Independently re-validates the per-tx max and allowlists when signing. A compromised Tally server can’t sneak a payment past this check.
status: "failed" on the resulting payment.
Errors
| Status | type | code | When |
|---|---|---|---|
| 400 | validation_failed | amount_invalid | amount_usdc is malformed. |
| 400 | validation_failed | address_invalid | wallet or to isn’t a valid EVM address. |
| 400 | validation_failed | — | Body shape failed Zod. details has the issues. |
| 401 | unauthenticated | — | API key invalid. |
| 403 | forbidden | amount_too_large | Exceeds the grant’s per-tx max. |
| 403 | forbidden | daily_cap_exceeded | Would push 24h spend over the cap. |
| 403 | forbidden | recipient_not_allowed | to is not in the grant’s recipient allowlist. |
| 403 | forbidden | contract_not_allowed | Destination contract isn’t in the allowlist (USDC is default). |
| 403 | forbidden | permission_expired | The grant’s expires_at has passed. |
| 404 | not_found | agent_not_found | No agent with that id in this (account, mode). |
| 404 | not_found | wallet_not_found | The wallet isn’t owned by this account. |
| 404 | not_found | permission_not_found | The agent has no active grant on the wallet. |
| 429 | rate_limited | rate_limit_exceeded | Payments bucket exceeded (30/min). |
| 500 | internal | privy_call_failed | Privy’s API failed during signing. Retry with the same idempotency key. |
Rate limit
POST /v1/payments uses the stricter payments bucket: 30 requests / 60 seconds per API key. See Rate limits.
GET /v1/payments/{id}
Retrieve a single payment by its Tally id. If the payment is still pending on Tally’s side and has a tx_hash, this call lazily refreshes from the chain — so polling GET /v1/payments/{id} is the canonical way to wait for a confirmation.
Request
| Path param | Description |
|---|---|
id | Tally’s internal payment id (the id field from a previous response). |
Response
200 OK. Same shape as the POST /v1/payments response. status reflects the latest known state — if the chain has confirmed since the last call, status will have flipped to confirmed or failed.
Errors
| Status | type | When |
|---|---|---|
| 401 | unauthenticated | API key invalid. |
| 404 | not_found | No payment with that id, or it belongs to a different (account, mode). |
| 429 | rate_limited | Default bucket exceeded. |
Patterns
Poll until confirmed
Production: subscribe to webhooks instead
For production workloads, replace the polling loop with a webhook subscription topayment.confirmed and payment.failed. The event carries the same payment shape; verification helper lives in the SDK webhooks page.
Not yet exposed
GET /v1/payments— listing with status/agent/direction filters.POST /v1/payments/{id}/refund— first-class refund handling.