> For the complete documentation index, see [llms.txt](https://help.multisig.ledger.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.multisig.ledger.com/guides/api-guides/4.-delegate-management.md).

# 4. Delegate Management

## What you'll learn

* What delegates are and when to use them
* Add a delegate for an owner using a signed API call
* List all delegates for a Safe
* Remove a delegate

## Prerequisites

* **Node.js 18+**
* **A Safe on a supported chain** where you control an owner key
* **A private key** for a Safe owner: testnet only
* **viem**: used to create a wallet client for signing delegate API requests

Install dependencies:

```bash
npm install @safe-global/api-kit viem
```

## Configuration

{% tabs %}
{% tab title="TypeScript" %}

```typescript
import SafeApiKitModule from "@safe-global/api-kit";
import { createWalletClient, http } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

// ESM interop-safe constructor resolution (required in some runtimes)
const SafeApiKit =
  typeof SafeApiKitModule === "function"
    ? SafeApiKitModule
    : (SafeApiKitModule as unknown as { default: typeof SafeApiKitModule }).default;

const CHAIN_ID = 11155111n; // Sepolia
const TX_SERVICE_URL = `https://app.multisig.ledger.com/api/safe-transaction-service/${CHAIN_ID}`;
const RPC_URL = "https://ethereum-sepolia-rpc.publicnode.com";
const SAFE_ADDRESS = "0xYourSafeAddress";
const OWNER_PRIVATE_KEY = "0xYourPrivateKey" as `0x${string}`; // Testnet only!

const apiKit = new SafeApiKit({
  chainId: CHAIN_ID,
  txServiceUrl: TX_SERVICE_URL,
});
```

{% endtab %}

{% tab title="Python" %}

```python
import time
import requests
from eth_account import Account
from eth_account.messages import encode_structured_data

CHAIN_ID = 11155111  # Sepolia
TX_SERVICE_URL = f"https://app.multisig.ledger.com/api/safe-transaction-service/{CHAIN_ID}"
SAFE_ADDRESS = "0xYourSafeAddress"
OWNER_PRIVATE_KEY = "your-private-key"  # Testnet only!

session = requests.Session()
session.headers.update({"accept": "application/json"})

owner_address = Account.from_key(OWNER_PRIVATE_KEY).address

def totp() -> int:
    # T0=0, Tx=3600. Time-based window in hours.
    return int(time.time()) // 3600

def sign_delegate_request(delegate_address: str) -> str:
    typed_data = {
        "types": {
            "EIP712Domain": [
                {"name": "name", "type": "string"},
                {"name": "version", "type": "string"},
                {"name": "chainId", "type": "uint256"},
            ],
            "Delegate": [
                {"name": "delegateAddress", "type": "address"},
                {"name": "totp", "type": "uint256"},
            ],
        },
        "primaryType": "Delegate",
        "domain": {
            "name": "Safe Transaction Service",
            "version": "1.0",
            "chainId": CHAIN_ID,
        },
        "message": {
            "delegateAddress": delegate_address,
            "totp": totp(),
        },
    }

    signable = encode_structured_data(primitive=typed_data)
    sig = Account.sign_message(signable, private_key=OWNER_PRIVATE_KEY).signature.hex()
    return sig
```

{% endtab %}

{% tab title="curl" %}

```bash
export CHAIN_ID=11155111  # Sepolia
export TX_SERVICE_URL="https://app.multisig.ledger.com/api/safe-transaction-service/${CHAIN_ID}"
export SAFE_ADDRESS="0xYourSafeAddress"
export OWNER_ADDRESS="0xOwnerAddress"
export DELEGATE_ADDRESS="0xDelegateAddress"

# You must compute:
# - totp = floor(unix_time_seconds / 3600)
# - signature = EIP-712 signature over (delegateAddress, totp)
export TOTP="0"
export SIGNATURE="0xOwnerSignature"
```

{% endtab %}
{% endtabs %}

> This example uses `sepolia` for the viem wallet client. If you switch `CHAIN_ID`, use the matching viem `chain` object.

The `TX_SERVICE_URL` points at the **Ledger-hosted** Transaction Service. Delegate records stored here are visible in the [Ledger Multisig UI](https://app.multisig.ledger.com/).

**Supported chains:** Ethereum (1), Optimism (10), BSC (56), Polygon (137), Base (8453), Arbitrum (42161), Sepolia (11155111).

## Step-by-step

{% stepper %}
{% step %}

### Set up the owner wallet client

Delegate management requires the owner to sign API requests (proving they authorize the delegate). Create a viem wallet client for the owner:

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const ownerAccount = privateKeyToAccount(OWNER_PRIVATE_KEY);
const ownerWalletClient = createWalletClient({
  account: ownerAccount,
  chain: sepolia,
  transport: http(RPC_URL),
});
const ownerAddress = ownerAccount.address;
```

{% endtab %}

{% tab title="Python" %}

```python
# In Python you typically sign delegate API requests locally (EIP-712).
# The helper `sign_delegate_request(...)` in Configuration does that.
#
# owner_address is already derived from OWNER_PRIVATE_KEY in Configuration.
```

{% endtab %}

{% tab title="curl" %}

```bash
# Signing happens client-side.
# Use any EIP-712 capable signer to produce SIGNATURE.
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

### Add a delegate

Add a delegate address that can propose transactions on behalf of the owner. The `label` is a human-readable identifier stored with the delegate record.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const delegateAddress = "0xDelegateAddress";

await apiKit.addSafeDelegate({
  safeAddress: SAFE_ADDRESS,
  delegateAddress,
  delegatorAddress: ownerAddress,
  signer: ownerWalletClient,
  label: "Backend Service",
});
```

{% endtab %}

{% tab title="Python" %}

```python
delegate_address = "0xDelegateAddress"
label = "Backend Service"

signature = sign_delegate_request(delegate_address)

payload = {
    "safe": SAFE_ADDRESS,
    "delegate": delegate_address,
    "delegator": owner_address,
    "label": label,
    "signature": signature,
}

resp = session.post(f"{TX_SERVICE_URL}/v2/delegates/", json=payload)
resp.raise_for_status()
```

{% endtab %}

{% tab title="curl" %}

```bash
curl -sS -X POST \
  "${TX_SERVICE_URL}/v2/delegates/" \
  -H "accept: application/json" \
  -H "content-type: application/json" \
  -d '{
    "safe": "'"${SAFE_ADDRESS}"'",
    "delegate": "'"${DELEGATE_ADDRESS}"'",
    "delegator": "'"${OWNER_ADDRESS}"'",
    "label": "Backend Service",
    "signature": "'"${SIGNATURE}"'"
  }'
```

{% endtab %}
{% endtabs %}

> **REST API:** `POST /v2/delegates/`
>
> The request body includes the Safe address, delegate address, delegator (owner), label, and a signature proving the delegator authorized this action.

The delegate can now call `proposeTransaction` with `senderAddress` set to their own address.
{% endstep %}

{% step %}

### List delegates

Query all delegates for the Safe:

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const delegates = await apiKit.getSafeDelegates({
  safeAddress: SAFE_ADDRESS,
});

for (const d of delegates.results) {
  console.log("delegate:", d.delegate);
  console.log("delegator:", d.delegator);
  console.log("label:", d.label);
}
```

{% endtab %}

{% tab title="Python" %}

```python
resp = session.get(f"{TX_SERVICE_URL}/v2/delegates/?safe={SAFE_ADDRESS}")
resp.raise_for_status()
delegates = resp.json()

for d in delegates["results"]:
    print("delegate:", d["delegate"])
    print("delegator:", d["delegator"])
    print("label:", d.get("label"))
```

{% endtab %}

{% tab title="curl" %}

```bash
curl -sS -X GET \
  "${TX_SERVICE_URL}/v2/delegates/?safe=${SAFE_ADDRESS}" \
  -H "accept: application/json"
```

{% endtab %}
{% endtabs %}

> **REST API:** `GET /v2/delegates/?safe={address}`
> {% endstep %}

{% step %}

### Remove a delegate

Remove a delegate when they should no longer be able to propose on the owner's behalf:

{% tabs %}
{% tab title="TypeScript" %}

```typescript
await apiKit.removeSafeDelegate({
  delegateAddress,
  delegatorAddress: ownerAddress,
  signer: ownerWalletClient,
});
```

{% endtab %}

{% tab title="Python" %}

```python
# The DELETE request is also authorized by an EIP-712 signature.
# The typed data is the same as for adding a delegate (delegateAddress + totp).
signature = sign_delegate_request(delegate_address)

resp = session.delete(
    f"{TX_SERVICE_URL}/v2/delegates/{delegate_address}/",
    params={"delegator": owner_address, "signature": signature},
)
resp.raise_for_status()
```

{% endtab %}

{% tab title="curl" %}

```bash
curl -sS -X DELETE \
  "${TX_SERVICE_URL}/v2/delegates/${DELEGATE_ADDRESS}/?delegator=${OWNER_ADDRESS}&signature=${SIGNATURE}" \
  -H "accept: application/json"
```

{% endtab %}
{% endtabs %}

> **REST API:** `DELETE /v2/delegates/{delegateAddress}/`
>
> Requires a signature from the delegator (owner) proving they authorize the removal.

Verify the removal:

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const remaining = await apiKit.getSafeDelegates({
  safeAddress: SAFE_ADDRESS,
});
console.log("Delegates remaining:", remaining.count);
```

{% endtab %}

{% tab title="Python" %}

```python
resp = session.get(f"{TX_SERVICE_URL}/v2/delegates/?safe={SAFE_ADDRESS}")
resp.raise_for_status()
remaining = resp.json()
print("Delegates remaining:", remaining["count"])
```

{% endtab %}

{% tab title="curl" %}

```bash
curl -sS -X GET \
  "${TX_SERVICE_URL}/v2/delegates/?safe=${SAFE_ADDRESS}" \
  -H "accept: application/json"
```

{% endtab %}
{% endtabs %}
{% endstep %}
{% endstepper %}

## Key concepts

> **What are delegates?**
>
> A delegate is an address authorized to **propose transactions** to the Transaction Service on behalf of a Safe owner, without being an owner themselves.

**Why use delegates?**

* **Automation:** A backend service or bot can propose transactions without holding an owner key
* **Separation of concerns:** Proposers don't need signing authority; owners still approve and execute
* **No gas cost:** Adding and removing delegates are off-chain API calls, not on-chain transactions

**Important distinctions:**

* Delegates can **propose** but cannot **sign** or **execute**. Only owners can provide confirmations.
* Delegate records are stored in the Transaction Service, not on-chain. They only affect who can call the proposal API.
* Each delegate is associated with a specific **delegator** (owner). If the delegator is removed as an owner, their delegates lose proposal rights.

## Tips and pitfalls

{% hint style="info" %}

* **No gas required.** Delegate management is entirely off-chain. Adding, listing, and removing delegates are API calls to the Transaction Service. No on-chain transactions are involved.
* **Wallet client required for signing.** The `addSafeDelegate` and `removeSafeDelegate` methods require a `signer` parameter. This is a viem `WalletClient` that signs the API request body, proving the owner authorized the action.
* **ESM import gotcha.** In some ESM runtimes, using `import SafeApiKit from "@safe-global/api-kit"` directly can throw `TypeError: SafeApiKit is not a constructor`. Use the interop-safe constructor resolution shown in Configuration.
* **Delegates per owner.** Each delegate is tied to a specific owner (delegator). If you want a delegate to act on behalf of multiple owners, add them separately for each owner.
* **Label is metadata only.** The `label` field is stored alongside the delegate record for identification purposes. It has no functional effect.
  {% endhint %}

## Next steps

{% content-ref url="/pages/265f124e505d916cc3b14406fd71284a403032ce" %}
[5. ERC20 Token Transfers](/guides/api-guides/5.-erc20-token-transfers.md)
{% endcontent-ref %}
