Multisig Wallets

Bind a signing key to a multisig wallet

Enroll the authenticated admin as a signer, with either the address of a secp256k1 key or the P-256 public key of a passkey their signer app just created. Nothing happens on-chain yet: the enrollment waits for a peer validation (requested → validated), except for the wallet creator, who is validated on the spot since no peer can vouch for the first signer. Reaching the threshold in validated signers triggers the deployment; on an already-deployed wallet, a validation queues an operator addSigners instead. The address is withheld from reads until the enrollment is validated.

post/v1/multisig-wallets/{walletId}/signers

Request

  • Base URL: https://api.prod.keyban.io
  • URL: https://api.prod.keyban.io/v1/multisig-wallets/{walletId}/signers
  • 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

Request body

addressstring

EVM address of a secp256k1 signing key held by the signer app — the ERC-7913 signer as-is (20 bytes). Exactly one of address and publicKey must be provided.

publicKeystring

DER SubjectPublicKeyInfo (91 bytes) of a P-256 passkey, as returned by getPublicKey() at registration (require getPublicKeyAlgorithm() === -7). The server extracts the curve point, validates it, and composes the 84-byte ERC-7913 signer with the network's WebAuthn verifier — the signer bytes are never taken from the wire. Exactly one of address and publicKey must be provided.

commitmentstring required

SAS engagement: HMAC(nonceC ‖ signerBytes), 32 bytes. Binds the candidate's secret nonce before the validator's nonce is known, so the short code cannot be ground to a chosen value. The nonce itself is revealed later, never at enrollment. For a passkey, the app must commit over the composed 84-byte signer bytes.

Example request

{
  "address": "0x36bc36646e0f00e567a126e116f0d1a0dc21c810",
  "publicKey": "0x3059301306072a8648ce3d020106082a8648ce3d03010703420004abababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab",
  "commitment": "0xabababababababababababababababababababababababababababababababab"
}

Response

Enrollment recorded

idstring uuid required

Entity UUID

createdAtstring date-time required

Creation date (ISO 8601)

updatedAtstring date-time required

Last update date (ISO 8601)

status'requested' | 'validated' | 'active' | 'failed' required

Enrollment status: requested (address bound, awaiting a peer validation), validated (counts towards the threshold), active (in the on-chain signer set, can sign), failed (on-chain add exhausted its retries).

signerType'eoa' | 'webauthn' required

Kind of key behind this signer: eoa (20-byte secp256k1 address) or webauthn (84-byte P-256 passkey signer). Derived from the identity, never stored.

addressstring nullable

ERC-7913 identity of the signing key: a 20-byte EVM address for an eoa signer, the 84-byte verifier ‖ qx ‖ qy bytes for a webauthn one. Withheld while requested: the SAS a validator must type derives from it, so serving it here would let any client compute the code instead of reading it off the candidate's phone — the whole point of the comparison. Revealed once validated.

validatorNoncestring nullable

The challenge nonce, once a validator has challenged this enrollment. The candidate app reads it to derive the 4-digit SAS it displays; null before a challenge and after the ceremony settles.

validatedAtstring date-time nullable required

When a peer validated this enrollment. Null while requested.

Example response

{
  "status": "active",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Jeanne Martin",
    "email": "jeanne.martin@example.com",
    "image": "https://example.com/avatar.png"
  },
  "signerType": "eoa",
  "address": "0x36bc36646e0f00e567a126e116f0d1a0dc21c810",
  "validatorNonce": "0x2222222222222222222222222222222222222222222222222222222222222222"
}

Changes

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