Multisig Wallets

Sign a treasury transfer

Add the calling signer’s signature over the transfer relayHash, in the form the enrolled key produces. An eoa signer submits a 65-byte secp256k1 signature over the raw hash (sign({ hash }), never an EIP-191 personal_sign digest — the account recovers with ECDSA.tryRecover on the hash itself). A webauthn signer submits the abi-encoded passkey assertion whose clientDataJSON challenge is base64url(relayHash), verified server-side exactly as the on-chain verifier will. The signer must be active — validated but not yet on chain is not enough, the account would reject it.

The signature that completes the threshold hands the transfer to the relay; from then on it can no longer be cancelled.

post/v1/multisig-wallets/{walletId}/intents/{intentId}/approvals

Request

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

Path parameters

walletIdstring uuid required

The unique identifier of the multisig wallet

intentIdstring uuid required

The transfer to sign

Request body

signaturestring required

Signature over relayHash, in the form the enrolled key produces. For an eoa signer: secp256k1 r‖s‖v, exactly 65 bytes, over the raw hash — not an EIP-191 personal_sign digest. For a webauthn signer: the abi-encoded assertion (r, s, challengeIndex, typeIndex, authenticatorData, clientDataJSON) whose clientDataJSON challenge is base64url(relayHash), low-s normalized, UV flag set. Capped at 2048 bytes.

Example request

{
  "signature": "0x111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222221b"
}

Response

Signature recorded

idstring uuid required

Entity UUID

createdAtstring date-time required

Creation date (ISO 8601)

updatedAtstring 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.

totalAmountstring required

Sum of the leg amounts in USDC sub-units, as a decimal string. Derived, never stored.

relayNoncestring 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.

relayHashstring 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.

approvalCountinteger required

Signatures collected so far, out of the wallet threshold.

expiresAtstring date-time required

End of the signing window. Past it the intent expires on its next touch and the collected signatures are purged.

txHashstring 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.

errorstring nullable

Why the transfer failed, or what made its outcome undetermined. Null while pending or once executed.

Example response

{
  "status": "pending",
  "transfers": [
    {
      "agentWalletId": "550e8400-e29b-41d4-a716-446655440000",
      "targetAddress": "0xcb0c0dca4246bb0f6b3309d64e7e35002cc3af1f",
      "amount": "1500000",
      "destinationLabel": "Prestataire Acme",
      "destinationKind": "contact"
    }
  ],
  "totalAmount": "4000000",
  "fiat": {
    "currency": "EUR",
    "centiPerUsdc": 92
  },
  "relayNonce": "3",
  "relayHash": "0x4d2f8c1e9b7a3d5f0c2e8a6b4d1f9c3e7a5b8d2f4c6e9a1b3d5f7c9e2a4b6d8f",
  "approvalCount": 1,
  "txHash": "0x4d2f8c1e9b7a3d5f0c2e8a6b4d1f9c3e7a5b8d2f4c6e9a1b3d5f7c9e2a4b6d8f"
}

Changes

No recorded changes to this operation across all 1 revision of this API.