Safe Transaction Service

entity
safeapioff-chainmultisigsignaturestransaction-service

The Safe Transaction Service is Safe’s open-source off-chain backend for storing proposed and partially-signed Safe transactions and messages. It is the database layer that makes the multisig signing workflow asynchronous and distributed: owners can sign at different times, from different wallets, without ever touching the blockchain until the threshold is met.

Why it exists

Safe’s on-chain contract cannot track partially-signed transactions — a transaction is either on-chain (executed) or off-chain (not yet executed), with nothing in between. But multisig signing is inherently multi-step: owner A signs today, owner B signs tomorrow, owner C is offline until next week. The partial signatures have to live somewhere until enough accumulate to trigger an on-chain execution.

Without a shared storage layer, each owner would have to email or otherwise ferry their signature blob to the others, and the final executor would have to collect and concatenate them all manually. The Safe Transaction Service centralizes this: owners submit their signatures to a shared API, the service stores them, and any party can fetch the current state to see what’s missing or to execute when the threshold is met.

The same logic applies to Safe messages. A message is not on-chain by default; it needs somewhere to accumulate its signatures so the set can be retrieved and verified later.

API surface for messages

The @safe-global/api-kit package is the official TypeScript client. Two methods matter for message signing:

  • addMessage(safeAddress, { message, signature }) — proposes a new message. The message field is the EIP-712 typed-data object (or string); the signature field is the bytes-encoded first owner signature produced by buildSignatureBytes([signature]). The service computes the safeMessageHash from the message and records the proposal under that key.
  • addMessageSignature(safeMessageHash, signature) — adds an additional owner signature to an existing proposal. Subsequent owners call this instead of addMessage because the message is already stored; only the new signature needs to be sent.
  • getMessage(safeMessageHash) — retrieves the current state: the message, the list of signatures collected so far, and a derived status field.

The corresponding endpoints accept transaction proposals (proposeTransaction, confirmTransaction) with parallel semantics.

Authentication and deployment

The hosted service at safe.global requires an API key, which is documented at docs.safe.global/core-api/how-to-use-api-keys. Without a key, only read endpoints work. Write endpoints (addMessage, addMessageSignature, proposeTransaction, confirmTransaction) all require authentication.

The service is open-source. Any team can deploy its own instance against any chain, using the reference implementation at github.com/safe-global/safe-transaction-service. The Protocol Kit and API Kit can be configured to point at a custom deployment instead of the hosted service.

Not a consensus layer

The Safe Transaction Service stores signatures; it does not validate them (beyond basic format checks), and it does not enforce Safe-specific rules like thresholds. It is a dumb database. All the cryptographic and governance logic lives on-chain in the Safe contract.

This matters because the service is centralized (or federated, depending on deployment), and a compromised service could in principle drop signatures, refuse to serve them, or misorder them. But it cannot forge a valid multisig signature — the final verification happens on-chain via checkSignatures and ecrecover / isValidSignature, and the service has no private key material. The worst a malicious service can do is censor; it cannot forge.

Safe{Wallet} integration

Safe{Wallet} is the official Safe UI. It reads from and writes to the Safe Transaction Service by default, which means any signature submitted through the SDK to the service immediately appears in Safe{Wallet}‘s UI for the corresponding Safe. Messages appear at https://app.safe.global/transactions/messages?safe=<NETWORK>:<SAFE_ADDRESS>.

This is how the Protocol Kit + Safe{Wallet} workflow composes: a developer’s script produces the first signature and calls addMessage, the remaining owners log into Safe{Wallet}, see the pending message, click “Confirm”, and Safe{Wallet} internally calls addMessageSignature with their signatures. Once the threshold is met, any party can fetch the complete signature set from the service and pass it to a verifier.

Connections