Co-signer Setup Guide

This guide will walk you through setting up and running the Utila Co-signer.

This guide will walk you through setting up and running the Utila Co-Signer.
When you finish, the process is running on your machine or server and shows as running under Vault Settings → API → Co-signers.

For what the co-signer is and which mode to choose, see Utila Co-Signer.

If the CLI is already signed in and keys are downloaded, jump to step 6.

Prerequisites

  • Vault admin access, or an admin who can approve the service account role and the device
  • Access to the Utila console
  • A computer or server that can keep the co-signer process running (or a container)
  • Utila CLI 1.5.0 or later (install in step 1)

1. Install the CLI

Install Utila CLI 1.5.0 or later. See Utila CLI.

You’re done when utila --version prints a version of 1.5.0 or later, for example:

1.6.2

2. Create a service account

Create a service account and assign the Signer role. Wait for admin approval of the role.

See Creating a Service Account. For what a service account is, see API Service Accounts.

You’re done when the service account exists in Vault Settings → Service Accounts with the Signer role approved.

3. Sign the CLI in

Sign the CLI in with that service account. See CLI Authentication.

You’re done when utila auth whoami shows the service account email.

4. Choose where the CLI stores signing data

Do this before you register a device. The CLI saves the device key and downloaded signing keys in a local folder (called state). If you change this later, later commands can fail.

See CLI State.

5. Register a device, approve it, and download signing keys

A device is the identity the co-signer uses to sign.

  1. Register the device with the CLI
  2. An admin approves it in Vault Settings → Devices and signs it in the Utila mobile app
  3. Download the signing keys

See Device and Key Shares Setup Guide.

Registration prints a device ID and a fingerprint. Save both. You need the fingerprint for admin approval and the device ID for download and for cosigner run.

Download keys for every chain you need to sign (for example Ethereum and Solana use different key types). If you skip one, the co-signer can look fine but will not sign that chain.

You’re done when utila keys download exits without an error, and you have a device ID you can pass to the next step.

6. Start the co-signer

Pick one mode: sign everything, or ask your webhook first. See Utila Co-Signer for the difference.

Keep this command running in its own terminal (or as a service). Don’t use that same window for other CLI commands.

Sign all transactions

Use this for a first test. It signs every pending transaction assigned to this service account:

utila cosigner run --vault-id <vault-id> --device-id <device-id> --sign-all-txs

Webhook mode

Use this when your own service should approve or reject each transaction. This is not the same as Event Webhooks in the console.

utila cosigner run --vault-id <vault-id> --device-id <device-id> \
  --webhook-url https://example.com/cosigner-webhook

Your webhook receives the transaction (same shape as GetTransaction) and must reply with HTTP 200 OK and:

{
  "signTransaction": true
}
  • true: sign
  • false: cancel the transaction
  • Any status other than 200 does not cancel. The transaction stays waiting for a signature.

If the webhook is unreachable, times out, or returns a non-200 status, the co-signer does not sign and does not cancel. It logs the error, leaves the transaction pending, and tries again on the next poll.

You’re done when the process stays in the foreground and logs that it is using key shares and checking for awaiting transactions, for example:

[>] Using the following keyshares:
  - Key #<key-id>: Key Share #<share-id>
[>] Checking for awaiting transactions designated for the co-signer user (user ID: <id>)

7. Confirm it is running

In the Utila console, go to Vault Settings → API → Co-signers.

The new co-signer should show as running. If it does not, the process is not connected. Check the terminal for errors, then see Troubleshooting.

8. Send transactions to the co-signer

The co-signer only signs transactions your application creates through the API, with this service account named as the signer.

Note: Transfers you create in the Utila console are not sent to the co-signer.

Two settings are easy to mix up:

SettingWhat it doesWhere
PolicyIs this transfer allowed? Does a person need to approve first?Console: vault transaction policy
designatedSignersWhich account must signInitiateTransaction API

Putting the service account on a policy as an approver is not enough. It needs the Signer role, the policy must allow the transfer, and the API request must list it in designatedSigners.

  • Policy Allow: eligible signers can sign with no extra approvals
  • Policy Requires approval: people approve first, then the co-signer can sign

See Edit the transaction policy for a vault.

Use the format users/{email} or users/{id}. Get a token with utila auth print-access-token (tokens last 1 hour).

curl -X POST https://api.utila.io/v2/vaults/<vault-id>/transactions:initiate \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "details": {
      "assetTransfer": {
        "asset": "assets/native.ethereum-mainnet",
        "amount": "0.01",
        "source": "vaults/<vault-id>/wallets/<source-wallet-id>",
        "destination": "0x..."
      }
    },
    "designatedSigners": [
      "users/[email protected]"
    ]
  }'
import os
import requests

VAULT_ID = "<vault-id>"
TOKEN = os.environ["UTILA_API_TOKEN"]  # from: utila auth print-access-token

resp = requests.post(
    f"https://api.utila.io/v2/vaults/{VAULT_ID}/transactions:initiate",
    headers={
        "Authorization": f"Bearer {TOKEN}",
        "Content-Type": "application/json",
    },
    json={
        "details": {
            "assetTransfer": {
                "asset": "assets/native.ethereum-mainnet",
                "amount": "0.01",
                "source": f"vaults/{VAULT_ID}/wallets/<source-wallet-id>",
                "destination": "0x...",
            }
        },
        "designatedSigners": [
            "users/[email protected]"
        ],
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.json()["transaction"]["name"])

A successful initiate response includes a transaction resource name. The co-signer should then pick it up (watch the co-signer terminal). If the transaction stays pending, see Troubleshooting.

Troubleshooting

SymptomLikely causeFix
Can’t install or run the CLIBinary not executable, or macOS/Windows blocked itSee Utila CLI
Auth fails after creating the service accountPublic key on the account does not match the private key you are usingSee CLI Authentication and Creating a Service Account
cipher: message authentication failedState location or encryption changed after you registered the deviceSee CLI State; use the same settings for register, download, and run
Device registered but cannot download keysApproved in the console but not signed in the mobile app (or the reverse)Complete both steps in Device and Key Shares Setup Guide
Co-signer not listed as runningProcess not connected, or looking at the wrong vaultVault Settings → API → Co-signers; check the terminal for errors
Co-signer is running, but transactions stay pendingCreated in the console, or designatedSigners missing / wrong formatCreate via API with "users/{sa-email}"
Policy looks correct, still not signingService account set as a policy approver, not named as signerKeep policy for allow/approve; name the signer on the API request
Webhook mode: transaction stays pendingWebhook unreachable, timed out, or returned a non-200 statusCheck webhook logs and the co-signer terminal; the transaction is not cancelled. Fix the endpoint and wait for the next poll
Works on some chains onlySigning keys downloaded for only one key typeDownload keys for every chain you need
CLI looks stuck / mixed with other commandsCo-signer is using the same terminalRun the co-signer in its own window

What's next

  • Send a small test transfer with step 8 and confirm it reaches Confirmed in the console.
  • If you used webhook mode, keep that endpoint highly available. An outage leaves transactions waiting; it does not cancel them.
  • Subscribe to Event Webhooks if you want your app notified when transaction state changes (this is separate from the co-signer webhook).
  • Run the co-signer as a long-lived service or container so it stays up.

Related