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.
- Register the device with the CLI
- An admin approves it in Vault Settings → Devices and signs it in the Utila mobile app
- 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-txsWebhook 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-webhookYour webhook receives the transaction (same shape as GetTransaction) and must reply with HTTP 200 OK and:
{
"signTransaction": true
}true: signfalse: cancel the transaction- Any status other than
200does 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:
| Setting | What it does | Where |
|---|---|---|
| Policy | Is this transfer allowed? Does a person need to approve first? | Console: vault transaction policy |
designatedSigners | Which account must sign | InitiateTransaction 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
| Symptom | Likely cause | Fix |
|---|---|---|
| Can’t install or run the CLI | Binary not executable, or macOS/Windows blocked it | See Utila CLI |
| Auth fails after creating the service account | Public key on the account does not match the private key you are using | See CLI Authentication and Creating a Service Account |
cipher: message authentication failed | State location or encryption changed after you registered the device | See CLI State; use the same settings for register, download, and run |
| Device registered but cannot download keys | Approved 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 running | Process not connected, or looking at the wrong vault | Vault Settings → API → Co-signers; check the terminal for errors |
| Co-signer is running, but transactions stay pending | Created in the console, or designatedSigners missing / wrong format | Create via API with "users/{sa-email}" |
| Policy looks correct, still not signing | Service account set as a policy approver, not named as signer | Keep policy for allow/approve; name the signer on the API request |
| Webhook mode: transaction stays pending | Webhook unreachable, timed out, or returned a non-200 status | Check 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 only | Signing keys downloaded for only one key type | Download keys for every chain you need |
| CLI looks stuck / mixed with other commands | Co-signer is using the same terminal | Run 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
- Authentication: mint API tokens
- CLI Configuration: default vault and device
- Downloads: CLI binaries
- Dockerfile: run the CLI in a container
