> 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 %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://help.multisig.ledger.com/guides/api-guides/4.-delegate-management.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
