Examples on this page use $API_URL. Set it once, with the /v1 prefix
included — every route lives under it:
export API_URL=https://<your-api-host>/v1Signing needs a token carrying the sign scope. All four signing routes address
a public key directly — you never walk account → wallet → address to sign.
Sign a message
curl -X POST "$API_URL/sign/message" \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{"address":"0xabc…","message":"hello from tee-docker"}'{ "address": "0xabc…", "signature": "0x…" }Choosing an encoding
Omit encoding and the address signs with its VM’s natural default — which
works for every address kind. Name one explicitly when the verifier expects a
particular scheme, keeping in mind that each explicit encoding belongs to one
address kind:
encoding |
Works on | Use when |
|---|---|---|
personal_sign |
EVM only | The verifier expects the EIP-191 personal-message prefix |
raw |
EVM only | The verifier expects a keccak-256 hash with no EIP-191 prefix |
ed25519 |
Solana only | Signing with an ed25519 key |
Asking for an encoding the address cannot perform returns 501
unsupported_operation.
message is always read as UTF-8 text, whatever the encoding. The string
"0xdeadbeef" is signed as those ten characters, not as the four bytes they
spell — the one exception is raw, which decodes its input as hex and therefore
requires 0x-prefixed hex of even length.
With an explicit encoding the response also carries messageHash, so you can
check what was actually signed:
{ "address": "0xabc…", "signature": "0x…", "messageHash": "0x…" }For personal_sign and raw that is the digest actually signed. For ed25519
it is the message bytes as hex — ed25519 signs the message directly rather than a
prehash, so there is no digest to report.
Sign typed data (EIP-712)
curl -X POST "$API_URL/sign/typed-data" \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{
"address": "0xabc…",
"typedData": {
"domain": { "name": "MyDapp", "version": "1", "chainId": 1, "verifyingContract": "0xdef…" },
"types": { "Order": [ { "name": "maker", "type": "address" }, { "name": "amount", "type": "uint256" } ] },
"primaryType": "Order",
"message": { "maker": "0xabc…", "amount": "1000" }
}
}'The response includes both intermediate hashes, so you can verify the domain binding independently:
{
"address": "0xabc…",
"signature": "0x…",
"domainSeparator": "0x…",
"structHash": "0x…"
}chainId is never guessed
chainId is optional. When omitted, the signature is bound to the address’s
own network, not to a default chain.
Typed-data signing is EVM-only. Calling it with a non-EVM address returns 422
with unsupported_for_kind.
Three more things worth knowing before you debug a payload that will not sign.
Send typedData as an object — eth_signTypedData_v4 carries it already
stringified in its second RPC parameter, and forwarding that string is rejected.
Every address inside the payload must be all-lowercase, all-uppercase, or
correctly EIP-55 checksummed; a mixed-case address with a bad checksum is
rejected, including the example verifyingContract printed in the EIP-712
specification itself. And if you forward a types.EIP712Domain list, keep it consistent with your
domain — the service does not cross-check the two, but that list is what the
signing engine encodes the domain from, so a mismatch changes or breaks the
result. The Sign reference has the details.
Failures worth handling
| Code | Status | What happened |
|---|---|---|
scope_denied |
403 | Token lacks sign |
account_not_found |
404 | No such address in this workspace |
account_locked |
423 | The owning account is locked — its own password, an explicit lock, or a lapsed custody window. Unlock it |
unsupported_for_kind |
422 | Typed data on a non-EVM address |
unsupported_operation |
501 | An encoding this address kind cannot perform |
invalid_body |
400 | Malformed request shape. The message names the fields that failed |
chain_id_mismatch |
400 | domain.chainId disagrees with the signing chain |
invalid_parameter |
400 | The signing engine rejected the payload contents — a bad field type, or a failed address checksum |
An address that exists but belongs to another tenant returns account_not_found
— identical to one that does not exist anywhere.
Related
- Scopes and permissions — why
signis separate fromwrite - Sign endpoints — exact shapes, including digest and bytes signing