Monitoring a Safe multisig with the Safe Transaction Service API
By Chainitor · Published
A Safe multisig keeps its configuration on-chain: owners, threshold, modules and guard. You can read all of that over plain JSON-RPC. The transactions waiting to be signed are a different matter. When an owner proposes a transaction in the Safe{Wallet} interface, the proposal and each signature are posted to Safe’s Transaction Service, an off-chain API. Nothing reaches the chain until the last signature is in and someone executes it.
That’s why the Transaction Service matters for security monitoring. If a malicious transaction is sitting in the queue with one of three signatures, the chain shows nothing. The API is the only place you can see it before it executes.
This guide covers the base URL and chain names, authentication (including a redirect that silently discards your key), the endpoints for configuration and the pending queue, and a small watcher you can run on a schedule.
Base URL and chain short names
Safe serves every supported chain from one host. The chain is selected by its EIP-3770 short name in the path:
https://api.safe.global/tx-service/{shortName}/api/...
| Chain | Short name | Base URL |
|---|---|---|
| Ethereum | eth | https://api.safe.global/tx-service/eth |
| Arbitrum One | arb1 | https://api.safe.global/tx-service/arb1 |
| OP Mainnet | oeth | https://api.safe.global/tx-service/oeth |
| Base | base | https://api.safe.global/tx-service/base |
| Polygon PoS | pol | https://api.safe.global/tx-service/pol |
| BNB Chain | bnb | https://api.safe.global/tx-service/bnb |
The API rejects lower-case addresses with Checksum address validation failed. Always send the EIP-55 checksummed form, for example from cast to-check-sum-address or ethers’ getAddress().
Authentication, and the redirect that drops your key
Safe’s API key documentation says to send the key as a bearer token:
curl -H "Authorization: Bearer $SAFE_API_KEY" \
"https://api.safe.global/tx-service/eth/api/v1/safes/0x849D52316331967b6fF1198e5E32A0eB168D039d/"
You create keys in Safe’s developer dashboard. Requests without a key still work at a low limit, documented as 2 requests per second and 5,000 per month, and the docs say that “any meaningful production usage requires authentication”. As of November 2026, Safe’s API pricing lists a free Builder plan (50K calls a month, 5 requests per second) and paid Growth and Scale plans. Safe’s API terms (§4.5) state that free API services are “solely intended for evaluation and testing purposes and not for productive use”, so budget for a paid plan if a production alert depends on it.
Two things will burn you:
1. The legacy hosts redirect, and your key does not follow. Older code and tutorials use per-chain hosts such as safe-transaction-mainnet.safe.global. Those now answer 308 Permanent Redirect to api.safe.global. Because that’s a different host, standard HTTP clients drop the Authorization header when they follow the redirect. curl does this, and so does axios, through follow-redirects. The request succeeds, but it runs unauthenticated. You won’t notice until you hit the 5,000-call ceiling. Use api.safe.global directly, and configure your client to refuse redirects so a stale URL fails loudly.
2. A bad key is not rejected. In a test on 12 November 2026, a request with an invalid bearer token returned 200 with the same data, served at the unauthenticated limit. Check the X-RateLimit-Limit response header instead. It reports your monthly quota. If it reads 5000, Safe did not recognise your key. Every response also carries X-RateLimit-Remaining and X-RateLimit-Reset, so you can alert before the quota runs out rather than after.
Reading the configuration
GET /api/v1/safes/{address}/
returns the Safe’s current state (abridged):
{
"address": "0x849D52316331967b6fF1198e5E32A0eB168D039d",
"nonce": "2870",
"threshold": 3,
"owners": ["0x964aE5aB…31E3", "0xeD5Ddb58…1cb9", "…"],
"modules": [],
"guard": "0x0000000000000000000000000000000000000000",
"fallbackHandler": "0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5",
"masterCopy": "0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552",
"version": "1.3.0"
}
Note that nonce is a string. Everything here is also on-chain, and you can cross-check it over RPC without spending API quota:
SAFE=0x849D52316331967b6fF1198e5E32A0eB168D039d
cast call $SAFE "getThreshold()(uint256)" -r $RPC
cast call $SAFE "getOwners()(address[])" -r $RPC
cast call $SAFE "getModulesPaginated(address,uint256)(address[],address)" \
0x0000000000000000000000000000000000000001 50 -r $RPC
For change detection, the on-chain route is often the better one. Every change emits an event from the Safe itself: AddedOwner, RemovedOwner, ChangedThreshold, EnabledModule, DisabledModule and ChangedGuard. You can collect them with eth_getLogs as described in the Transfer events guide. Use the API’s snapshot to set the baseline, and the events to catch each change.
Reading the pending queue
GET /api/v2/safes/{address}/multisig-transactions/?executed=false&nonce__gte={nonce}&ordering=nonce
executed=false excludes executed transactions. nonce__gte set to the Safe’s current nonce drops stale proposals that can no longer execute, because a lower nonce has already been used. Use the v2 path, since the v1 list endpoint is deprecated. Each result includes, among other fields:
| Field | Meaning |
|---|---|
nonce | The slot it would execute in. Several proposals can compete for one nonce. |
safeTxHash | A stable ID for the proposal. Use it to deduplicate alerts. |
to, value, data | What it calls. |
operation | 0 is a call, 1 is a delegatecall, which runs the target’s code with the Safe’s storage. |
dataDecoded | The decoded method and parameters when Safe knows the ABI, otherwise null. |
confirmations, confirmationsRequired | Signatures so far and the threshold. |
submissionDate | When it was proposed. |
Batches deserve special handling. A Safe{Wallet} batch is a delegatecall to a MultiSend contract, so operation: 1 is normal at the top level. The real calls are in dataDecoded.parameters[0].valueDecoded, and each has its own operation, to and dataDecoded. A check that looks only at the top level misses a dangerous call hidden inside a batch. Delegatecall was also the mechanism behind the February 2025 Bybit loss, when a transaction signed through a compromised interface delegatecalled into attacker code that replaced the Safe’s implementation (CertiK’s analysis).
A small watcher
This script uses Node 18+ and its built-in fetch. It prints the configuration and flags queued transactions that change the configuration or contain a delegatecall, including inside batches:
import { getAddress } from 'ethers'; // only for checksumming
const BASE = 'https://api.safe.global/tx-service/eth'; // eth, arb1, oeth, base, pol, bnb, ...
const SAFE = getAddress('0x849d52316331967b6ff1198e5e32a0eb168d039d');
const CONFIG_METHODS = new Set([
'addOwnerWithThreshold', 'removeOwner', 'swapOwner', 'changeThreshold',
'enableModule', 'disableModule', 'setGuard', 'setModuleGuard',
'setFallbackHandler', 'changeMasterCopy',
]);
async function safeApi(path) {
const res = await fetch(`${BASE}${path}`, {
headers: { Authorization: `Bearer ${process.env.SAFE_API_KEY}` },
redirect: 'error', // a redirect to another host would drop the key
});
if (!res.ok) throw new Error(`${res.status} ${path}: ${await res.text()}`);
return {
body: await res.json(),
limit: res.headers.get('x-ratelimit-limit'),
remaining: res.headers.get('x-ratelimit-remaining'),
};
}
const { body: info, limit, remaining } = await safeApi(`/api/v1/safes/${SAFE}/`);
if (limit === '5000') console.warn('Safe did not recognise the API key');
console.log({ threshold: info.threshold, owners: info.owners.length, modules: info.modules,
guard: info.guard, nonce: info.nonce, quota: `${remaining}/${limit}` });
const { body: queue } = await safeApi(
`/api/v2/safes/${SAFE}/multisig-transactions/?executed=false&nonce__gte=${info.nonce}&ordering=nonce&limit=100`,
);
// MultiSend batches carry their inner calls in dataDecoded.parameters[0].valueDecoded
const innerCalls = (tx) =>
tx.dataDecoded?.method === 'multiSend' ? (tx.dataDecoded.parameters?.[0]?.valueDecoded ?? []) : [tx];
for (const tx of queue.results) {
const flags = [];
for (const call of innerCalls(tx)) {
if (Number(call.operation) === 1) flags.push(`delegatecall to ${call.to}`);
if (CONFIG_METHODS.has(call.dataDecoded?.method)) flags.push(call.dataDecoded.method);
}
console.log(`nonce ${tx.nonce} ${tx.confirmations.length}/${tx.confirmationsRequired} ${tx.safeTxHash}`,
flags.length ? `FLAG: ${flags.join(', ')}` : '');
}
To turn this into monitoring, store the configuration snapshot and the safeTxHash values you’ve already seen. On each run, alert on any difference and on any new flagged proposal. Two refinements are worth adding early:
- Pin the MultiSend address. A top-level delegatecall is expected only when it targets Safe’s official MultiSend or MultiSendCallOnly deployment for that Safe version. A batch that delegatecalls anywhere else should alert by itself.
- Count your calls. One request is one unit of quota. Polling two endpoints once a minute for one Safe is 89,280 requests in a 31-day month. That’s more than the free Builder quota, before any retries. Poll the queue more often than the configuration, and read configuration changes from on-chain events, which cost no Safe quota.
Doing it with Chainitor
Chainitor’s Safe security monitors cover both layers:
- Configuration monitors alert when an owner is added or removed, the threshold changes, the guard changes, or a module is enabled or disabled. They read the Safe’s own events from the chain and don’t use the Safe API.
- The pending configuration change monitor reads the queue from the Transaction Service. It flags proposals that would change owners, threshold, guard or modules, and delegatecalls, including those inside MultiSend batches, before they collect enough signatures. A Safe activity monitor can also report new proposals, signatures and executions.
Those two queue-reading monitors call the Safe API with your own Safe API key, added under Settings → Integrations, so their calls count against your Safe quota. Chainitor shows the quota Safe reports and warns when the key is not recognised or the quota is running low. Safe monitors are on every plan. Checks run every 5 minutes on Starter and every minute on Pro, subject to your Safe API quota: Chainitor raises the minimum interval of these two monitors so they fit it. Alerts go to email, Slack, Telegram, PagerDuty or a webhook on every plan.
Related
- Safe wallet security monitors Configuration-change and pending-transaction monitors for Safe multisigs.
- Event monitor Alert on any contract event, including events a Safe emits on-chain.
- Watching ERC-20 Transfer events with eth_getLogs Track the tokens leaving the Safe as well as its configuration.
- All guides More hands-on guides to watching EVM chains.
Watch your Safes without maintaining the script
Chainitor alerts on Safe owner, threshold, guard and module changes, and on risky transactions waiting in the queue, by email, Slack, Telegram, PagerDuty or webhook.
Start monitoring free7-day free trial • No immediate charge