Skip to content

Sign messages, typed data, digests, and bytes

Four routes, addressed by public key rather than by account — the encoding options, what comes back, and which failures belong to the address.

Updated View as Markdown

Auth: workspace token. Scope: sign on every route.

All four address a public key directly. Resolution is scoped to the session’s own workspace, so one tenant can never sign with another’s key — and an address that exists elsewhere returns the same 404 as one that does not exist at all.

POST /v1/sign/message

Returns 200.

Request

{
  "address": "0xabc…",
  "message": "hello from tee-docker",
  "encoding": "personal_sign"
}
Field Type Required Notes
address string yes Public key, 1–128 characters
message string yes UTF-8 text for every encoding except raw, which decodes it as hex
encoding enum no personal_sign, raw, or ed25519 — each restricted to one address kind

How message is read depends on the encoding, and getting it backwards yields a valid signature over the wrong bytes. Every encoding except raw reads it as UTF-8 text, so "0xdeadbeef" is signed as those ten characters. raw decodes it as hex, so the same string is signed as the four bytes it spells.

Each explicit encoding belongs to one address kind, and asking for the wrong one returns 501 unsupported_operation:

encoding Address kind What is signed
omitted any The UTF-8 text, using that address’s own default scheme. No messageHash comes back.
personal_sign EVM only The UTF-8 text, with the standard Ethereum message prefix.
raw EVM only The keccak-256 hash of the decoded bytes, with no prefix. message must be 0x-prefixed hex of even length.
ed25519 Solana only The UTF-8 text, signed directly.

Omitting encoding works for both address kinds, so it is the portable choice when one code path signs with either.

Response

{ "address": "0xabc…", "signature": "0x…" }

With an explicit encoding, the response also carries messageHash:

{ "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.

POST /v1/sign/typed-data

EIP-712 typed data. EVM only. Returns 200.

Request

{
  "address": "0xabc…",
  "typedData": {
    "domain": { "name": "MyDapp", "version": "1", "chainId": 1, "verifyingContract": "0xdef…" },
    "types": { "Order": [ { "name": "maker", "type": "address" } ] },
    "primaryType": "Order",
    "message": { "maker": "0xabc…" }
  },
  "chainId": 1
}
Field Type Required Notes
address string yes Public key, 1–128 characters
typedData object yes The EIP-712 payload, as an object — see below
chainId integer no Positive. Defaults to the address’s own network

typedData needs domain, types, primaryType and message. primaryType must exactly name one of the structs in types, matching case. Struct types you never reference are ignored.

This service does not inspect the domain object’s keys. It reads only the exact lowercase chainId for the chain check below, so a mis-cased key such as chainID — which a JSON round-trip can easily produce — is read as absent: no chain_id_mismatch fires on it, and the domain is passed to the signing engine as sent. Get the field names right; the service will not catch a typo here.

Four things that catch people out

Response

{
  "address": "0xabc…",
  "signature": "0x…",
  "domainSeparator": "0x…",
  "structHash": "0x…"
}

Both intermediate hashes are returned so you can verify the domain binding independently.

A non-EVM address returns 422 with unsupported_for_kind.

POST /v1/sign/digest

A raw secp256k1 signature over a caller-supplied 32-byte digest. EVM only. Returns 200.

This signs the digest verbatim — no keccak, no EIP-191 prefix. You RLP-encode and hash the transaction yourself, the service signs the digest, and you broadcast. It is the parity mode /sign/message’s raw encoding does not provide: raw hashes what you send, this signs it as-is.

Request

{
  "address": "0xabc…",
  "digest": "0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658"
}
Field Type Required Notes
address string yes Public key, 1–128 characters
digest string yes Lowercase 0x-hex of exactly 32 bytes (^0x[0-9a-f]{64}$)

The digest format is strict on purpose: a signature is a bearer instrument, so an ambiguous or wrong-length input is refused rather than padded or re-hashed. Unknown fields are rejected.

Response

{
  "address": "0xabc…",
  "r": "0x…",
  "s": "0x…",
  "recovery": 0,
  "signature": "0x…"
}

signature is the 65-byte r ‖ s ‖ v form with v = 27 + recovery. recovery is the yParity (0 or 1) on its own, so you can build whichever v your transaction type needs.

A non-EVM address returns 422 with unsupported_for_kind.

POST /v1/sign/bytes

A raw ed25519 signature over caller-supplied bytes. Solana only. Returns 200.

This signs the exact bytes verbatim. You compile the Solana message yourself, the service signs it, and you attach the signature and broadcast.

Request

{
  "address": "So1abc…",
  "message": "0x0102feff",
  "encoding": "hex"
}
Field Type Required Notes
address string yes Public key, 1–128 characters
message string yes The bytes to sign, 1 byte to 16 KiB. Lowercase 0x-hex of whole bytes by default, or base64
encoding enum no hex (default) or base64

hex must be lowercase, 0x-prefixed, and an even number of digits. base64 must round-trip exactly — a lenient value that decodes to different bytes than it re-encodes to is rejected rather than signed. Both name the same bytes; the encoding only says how message is written. Unknown fields are rejected.

Response

{
  "address": "So1abc…",
  "signature": "5f2…",
  "signatureHex": "0x…"
}

signature is the 64-byte ed25519 signature as base58 (the form a Solana transaction carries); signatureHex is the same 64 bytes as 0x-hex.

A non-Solana address returns 422 with unsupported_for_kind.

Errors

Status Code When
400 invalid_body The body or the EIP-712 envelope is the wrong shape. The message names what failed.
400 chain_id_mismatch domain.chainId disagrees with the chain being signed for.
400 invalid_parameter The signing engine rejected the payload — a bad field type, or an address failing its checksum.
401 session_expired
403 scope_denied The token lacks sign, or it is a scoped API key (the sign routes are workspace-token-only).
404 account_not_found No such address in this workspace.
422 unsupported_for_kind Typed data or a digest on a non-EVM address, or bytes on a non-SVM address.
423 account_locked
501 unsupported_operation An encoding this address kind cannot perform.

invalid_body means this service rejected the request shape; invalid_parameter means it passed that check and the signing engine rejected the contents. The split is useful when narrowing down a payload that will not sign.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close