How requests and signing work
Your API key does two jobs. It signs a short challenge to get you a token, and it signs each transaction that moves funds out of your wallet. The two signatures use different formats, so this page shows each one.
Authenticate a request
To sign in, sign the string {key_id}.{timestamp} with your API key and exchange the signature for tokens at POST /v1/auth/api-key/token. The access token lasts an hour, and the refresh token gets you a new pair without signing again.
- Use the current Unix time in seconds. It must be within 30 seconds of ours.
- Sign with ES256, which is ECDSA over P-256 with SHA-256.
- Send
randsconcatenated, base64url-encoded, assignature.
Send the access token as Authorization: Bearer on every call. You never send TM-On-Behalf-Of; that header is for businesses acting for their users.
Getting started, step 2 has the signing code.
- TypeScript
- Python
- curl
const balances = await get("/v1/gateway/balances");
balances = get("/v1/gateway/balances")
curl -s https://api.truemarkets.co/v1/gateway/balances \
-H "Authorization: Bearer $TOKEN"
Sign a wallet transaction
Anything that moves funds, such as an order, a cancel on-chain or a transfer, comes back with unsigned payloads. Each payload is one wallet transaction, and nothing moves until you sign it and execute. You can sign with any P-256 library:
- Take each
payloadstring exactly as the response returned it. Don't parse or re-serialize it. - Sign it with ECDSA over P-256 and SHA-256, using your API key. Encode the signature as DER, then as hex.
- Put it in a JSON object with your compressed public key, as 66 hex characters, and the scheme
SIGNATURE_SCHEME_TK_API_P256. - Base64url-encode that JSON. The result is one entry in
signatures[], in the same order aspayloads. - Post
signaturesto the execute call withauth_type: "api_key".
When you created the key, the app registered it on your wallet, which is why the same key works here. Your key file holds it as a JWK; the hex values the libraries below take are its d field and its compressed public key.
{
"publicKey": "02a1b2c3…7e8f90",
"signature": "3045022100…",
"scheme": "SIGNATURE_SCHEME_TK_API_P256"
}
- TypeScript
- Python
// One key file does both jobs: it signs the token challenge, and,
// because the app registered it on your wallet, your payloads.
const { key_id, private_key: jwk } = JSON.parse(
readFileSync(KEY_FILE, "utf8"),
);
const y = Buffer.from(jwk.y, "base64url");
const stamper = new ApiKeyStamper({
// The compressed public key and the private key, as hex.
apiPublicKey: Buffer.concat([
Buffer.from([y[31] % 2 === 0 ? 2 : 3]),
Buffer.from(jwk.x, "base64url"),
]).toString("hex"),
apiPrivateKey: Buffer.from(jwk.d, "base64url").toString("hex"),
});
const stamp = async (payload: string) =>
(await stamper.stamp(payload)).stampHeaderValue;
# One key file does both jobs: it signs the token challenge, and,
# because the app registered it on your wallet, your payloads.
with open(KEY_FILE) as f:
key_file = json.load(f)
key_id, jwk = key_file["key_id"], key_file["private_key"]
y = b64url_bytes(jwk["y"])
stamper = ApiKeyStamper(
ApiKeyStamperConfig(
# The compressed public key and the private key, as hex.
api_public_key=(
bytes([2 if y[31] % 2 == 0 else 3])
+ b64url_bytes(jwk["x"])
).hex(),
api_private_key=b64url_bytes(jwk["d"]).hex(),
)
)
def stamp(payload):
return stamper.stamp(payload).stamp_header_value
Or use a Turnkey library
| Language | Library | Call |
|---|---|---|
| TypeScript | @turnkey/api-key-stamper | new ApiKeyStamper({ apiPublicKey, apiPrivateKey }).stamp(payload) |
| Go | github.com/tkhq/go-sdk/v2 | NewAPIKeyStamper(privateKey).Stamp(ctx, payload) |
| Python | turnkey-api-key-stamper | ApiKeyStamper(ApiKeyStamperConfig(api_public_key, api_private_key)).stamp(payload) |
| Rust | turnkey_api_key_stamper | see the crate docs |
Each one returns the same base64url stamp. Pass the payload string exactly as you received it.
Getting started, step 4 converts your key file and uses the TypeScript one end to end.
Next: Funding