---
title: "Open a treasury transfer lot for signature"
method: POST
path: "/v1/multisig-wallets/{walletId}/intents"
tags: ["Multisig Wallets"]
---

# Open a treasury transfer lot for signature

`POST /v1/multisig-wallets/{walletId}/intents`

## Request

- Base URL: `https://api.prod.keyban.io`
- URL: `https://api.prod.keyban.io/v1/multisig-wallets/{walletId}/intents`
- Auth: one of:
  - API key in header `X-Api-Key`
  - `bearer` (a scheme the document does not define)

Create a lot of 1 to 20 USDC transfers from the organization treasury,
and open the signing window. Each leg names EITHER an agent wallet of
the organization OR a contact of this treasury address book — never a
raw address. The lot is ONE signing subject: a single signature per
signer covers every leg, and execution is atomic — every leg lands or
none does. Nothing moves yet: the lot broadcasts once the wallet
threshold of signatures is collected from the signer apps.

The server owns every calldata — each leg is rebuilt from its validated
destination and amount — so this endpoint can only ever move USDC to an
agent wallet of the caller organization or a contact of this treasury,
never an arbitrary address. The response carries the structured
pre-image (`transfers`, `relayNonce`, `relayHash`): a signer app **must**
recompute the hash locally from those fields — the leg order is part of
it — and refuse to sign on a mismatch, so a tampered server cannot get
something else signed. Each leg also carries `destinationLabel`,
`destinationKind` and `destinationFirstUse`, resolved from the frozen
address at read time for display only, and the lot carries `fiat`, an
indicative rate in the organization display currency: all of them are
outside the signed pre-image and must never enter that computation.

One live lot per treasury: the signatures are bound to a single account
nonce, so a second concurrent intent could never both execute. A previous
intent whose outcome is undetermined (`unknown`) must be reconciled
first. The creation is also refused when the treasury balance visibly
cannot cover the lot total.

## Path parameters

- `walletId` string, uuid, required

## Request body

- CreateMultisigIntentDto
  - `transfers` union[], required: Legs of the transfer lot, in the exact order the signers will see. Each leg names EITHER an agent wallet of the fleet OR a contact of this treasury. The lot is ONE signing subject — a single signature per signer covers all of it — and executes atomically: every leg lands or none does. 1 to 20 legs.
    - union
      - object
        - `agentWalletId` string, uuid, required: Agent wallet of the same organization and network to credit. Snapshotted on the intent, so renaming or deleting it later cannot change where the signed transfer goes.
        - `amount` string, required: Amount in USDC sub-units (6 decimals), as a positive integer string. "1500000" is 1.50 USDC.
      - object
        - `contactId` string, uuid, required: Contact of THIS treasury to pay. Naming a contact is the only way to reach an address outside the fleet: the API never accepts a raw address, so a destination is always one a signer vouched for first.
        - `amount` string, required: Amount in USDC sub-units (6 decimals), as a positive integer string. "1500000" is 1.50 USDC.

## Response `201`

Transfer awaiting signatures

- MultisigIntentDtoOutput
  - `id` string, uuid, required: Entity UUID
  - `createdAt` string, date-time, required: Creation date (ISO 8601)
  - `updatedAt` string, date-time, required: Last update date (ISO 8601)
  - `status` 'pending' | 'broadcasting' | 'unknown' | 'executed' | 'failed' | 'cancelled' | 'expired', required: Lifecycle: `pending` (collecting signatures), `broadcasting` (threshold reached, relay in progress), `executed` (mined, `txHash` set), `failed` (reverted or never broadcast), `unknown` (outcome undetermined — the transfer may have landed, and no new intent is accepted until it is reconciled), `cancelled`, `expired`.
  - `transfers` object[], required: Legs of the lot, in the exact order they were signed — the order is part of the signed preimage. The signer app displays the full list and re-derives every calldata from it.
    - `agentWalletId` string, uuid, nullable, required: Agent wallet this leg credits, as resolved at creation. Null when the leg targets a contact of the treasury instead, and on rows migrated from the single-transfer era whose agent wallet had already been deleted.
    - `targetAddress` string, required: On-chain address of that agent wallet, snapshotted so the destination stays what the signers saw even if the wallet row is later renamed or deleted.
    - `amount` string, required: Amount in USDC sub-units (6 decimals), as a positive integer string. "1500000" is 1.50 USDC.
    - `destinationLabel` string, nullable, required: Display name of this destination, resolved from the frozen `targetAddress` **at read time and never stored on the intent** — an agent wallet of the organization, else a contact of this treasury (archived ones included, so the history keeps its names). Null when the address resolves to neither. It is the frozen address above, not this label, that a signer verifies against.
    - `destinationKind` 'agent' | 'contact' | 'unknown', required: What the frozen address turned out to be: `agent` (a wallet of your own fleet — the leg is internal), `contact` (a named third party), `unknown` (neither, e.g. a wallet deleted since). Resolved from the address, so it stays true whatever path created the leg.
    - `destinationFirstUse` boolean, nullable, required: True when no transfer to this contact has ever EXECUTED — the signal the signer app puts in front of the signers, since a brand-new destination is where a compromised-channel address lands. **Null on an `agent` or `unknown` destination**: a refill toward one’s own fleet needs no such warning.
  - `totalAmount` string, required: Sum of the leg amounts in USDC sub-units, as a decimal string. Derived, never stored.
  - `fiat` object, nullable, required: Indicative rate for display only, resolved **at read time and never stored on the intent**: it is outside the signed preimage and must never enter its computation. Null when the organization has no display currency or the rate oracle is unavailable — the lot is still fully served and signable.
    - `currency` 'EUR' | 'USD' | 'JPY', required: Display currency of the organization at read time.
    - `centiPerUsdc` integer, required: Centi-units of `currency` per 1 USDC when the lot was served. A leg converts as floor(amount × centiPerUsdc / 1e6) centi-units.
  - `relayNonce` string, required: Account relay nonce the signatures are bound to, as a decimal string. One nonce covers the whole lot. If the account moves past it, the intent fails without broadcasting.
  - `relayHash` string, required: What each signer signs. Single-leg lot: `keccak256(abi.encode(chainId, account, nonce, dest, value, keccak256(data)))`. Two legs or more: `keccak256(abi.encode(chainId, account, nonce, keccak256(abi.encode(calls))))` over the ordered `(dest, value, data)` legs. The app must recompute it locally from the other fields and refuse to sign on a mismatch.
  - `approvalCount` integer, required: Signatures collected so far, out of the wallet threshold.
  - `expiresAt` string, date-time, required: End of the signing window. Past it the intent expires on its next touch and the collected signatures are purged.
  - `txHash` string, nullable: Hash of the relayed transaction. Null until executed — and can stay null on an intent reconciled as `executed` after an ambiguous broadcast, where only the consumed account nonce proves execution.
  - `error` string, nullable: Why the transfer failed, or what made its outcome undetermined. Null while pending or once executed.

## Other responses

- `403`: Session-authenticated user required
- `404`: Multisig wallet not found
- `409`: A transfer is already awaiting signatures or unresolved, the treasury is not active, or no known USDC on this network
- `422`: Invalid transfer destination, or the lot exceeds the treasury's USDC balance

---

[API](https://skmtc.dev/keyban/apis/keyban-rest-apis.md) · [All operations](https://skmtc.dev/keyban/apis/keyban-rest-apis/llms.txt) · [OpenAPI document](https://skmtc.dev/keyban/apis/keyban-rest-apis/revisions/0bb79a1da961?raw)
