Skip to main content

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.

Your codeTrue Markets
Sign inwith your API key
Sign {key_id}.{timestamp}POST /v1/auth/api-key/token
Access and refresh tokensrefresh without signing again
Place the orderwith your token
Create the orderPOST /v1/gateway/orders
Unsigned payloadsone wallet transaction each
Sign and executewith the same API key
Stamp each payloadstays on your machine
Execute with the signaturesPOST /v1/gateway/orders/{id}/execute
Order statuscomplete, or pending until final

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.

  1. Use the current Unix time in seconds. It must be within 30 seconds of ours.
  2. Sign with ES256, which is ECDSA over P-256 with SHA-256.
  3. Send r and s concatenated, base64url-encoded, as signature.

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.

The helper adds your token
const balances = await get("/v1/gateway/balances");

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:

  1. Take each payload string exactly as the response returned it. Don't parse or re-serialize it.
  2. Sign it with ECDSA over P-256 and SHA-256, using your API key. Encode the signature as DER, then as hex.
  3. Put it in a JSON object with your compressed public key, as 66 hex characters, and the scheme SIGNATURE_SCHEME_TK_API_P256.
  4. Base64url-encode that JSON. The result is one entry in signatures[], in the same order as payloads.
  5. Post signatures to the execute call with auth_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.

One signatures[] entry, before base64url encoding
{
"publicKey": "02a1b2c3…7e8f90",
"signature": "3045022100…",
"scheme": "SIGNATURE_SCHEME_TK_API_P256"
}
Turn your key file into the stamper's hex keys
// 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;

Or use a Turnkey library​

LanguageLibraryCall
TypeScript@turnkey/api-key-stampernew ApiKeyStamper({ apiPublicKey, apiPrivateKey }).stamp(payload)
Gogithub.com/tkhq/go-sdk/v2NewAPIKeyStamper(privateKey).Stamp(ctx, payload)
Pythonturnkey-api-key-stamperApiKeyStamper(ApiKeyStamperConfig(api_public_key, api_private_key)).stamp(payload)
Rustturnkey_api_key_stampersee 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