Skip to main content

How requests and signing work

Gateway uses two keys for two different jobs, and they never swap. Your organization API key proves who is calling. Your signer key approves each transaction that moves funds out of a user's wallet.

Your serverTrue Markets
Sign inwith organization API key
Sign {key_id}.{timestamp}POST /v1/auth/api-key/token
Organization tokenlasts one hour, no refresh token
Place the orderwith token + TM-On-Behalf-Of
Create the order for a userPOST /v1/gateway/orders
Unsigned payloadsone wallet transaction each
Sign and executewith signer key
Stamp each payloadstays on your server
Execute with the signaturesPOST /v1/gateway/orders/{id}/execute
Order statuscomplete, or pending until final

Which key does what​

Organization API keySigner key
Provesthat the request comes from your organizationthat you approve one wallet transaction
Comes fromthe developer consoleyou generate it; the console can make a test one
Its public half liveson your organizationon each user, as signer_public_key
Signsthe string {key_id}.{timestamp}each payload an order, cancel or transfer returns
Sent assignature on POST /v1/auth/api-key/tokensignatures[] on an execute call

A signature made with the wrong key is rejected, so keep the two apart in your code.

Authenticate a request​

To sign in, sign the string {key_id}.{timestamp} with your organization API key and exchange the signature for an organization token at POST /v1/auth/api-key/token. The token lasts an hour and has no refresh token, so sign in again before it expires.

  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 token as Authorization: Bearer on every call. A call that acts for one of your users adds TM-On-Behalf-Of with their user_id. Calls about your organization as a whole, such as creating users or reading organization transactions, leave the header out.

Getting started, step 2 has the signing code.

The helpers add both headers
const balances = await get("/v1/gateway/balances", forUser);

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

The wallets run on Turnkey, which checks this signature and adds our approval before the transaction reaches the chain. Signer keys covers where to keep the key.

One signatures[] entry, before base64url encoding
{
"publicKey": "02a1b2c3…7e8f90",
"signature": "3045022100…",
"scheme": "SIGNATURE_SCHEME_TK_API_P256"
}
stamp.ts: node:crypto, no dependencies
import { createECDH, createPrivateKey, sign } from "node:crypto";
import { readFileSync } from "node:fs";

// A signer key file with hex keys, the format the console's
// Users page downloads.
const { signer_private_key } = JSON.parse(
readFileSync(process.env.SIGNER_KEY_FILE!, "utf8"),
);
const ecdh = createECDH("prime256v1");
ecdh.setPrivateKey(Buffer.from(signer_private_key, "hex"));
const point = ecdh.getPublicKey(); // 0x04 || x || y
const key = createPrivateKey({
format: "jwk",
key: {
kty: "EC",
crv: "P-256",
d: Buffer.from(signer_private_key, "hex").toString(
"base64url",
),
x: point.subarray(1, 33).toString("base64url"),
y: point.subarray(33).toString("base64url"),
},
});
const publicKey = ecdh.getPublicKey("hex", "compressed");

export function stamp(payload: string): string {
const signature = sign(
"sha256",
Buffer.from(payload),
key,
).toString("hex");
const json = JSON.stringify({
publicKey,
signature,
scheme: "SIGNATURE_SCHEME_TK_API_P256",
});
return Buffer.from(json).toString("base64url");
}

Download stamp.ts or stamp.py.

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 5 uses the TypeScript and Python ones end to end.

Next: Signer keys