Skip to main content

Getting started with Trading API

By the end you've minted a token, bought a small amount of SOL, and found the fill in your transactions. It takes about 10 minutes, plus the time your USDC transfer takes.

This guide trades real funds

We don't have a fully working sandbox yet, so every trade here is a real on-chain transaction. Keep the amounts small.

Before you begin​

You need the key file from Create your API key. It does two jobs: it signs you in, and it signs your own orders.

The code needs Node.js 18 or later, or Python 3.10 or later. Each step below is a part of one script, quickstart.ts or quickstart.py, that you run at the end.

Install, then point at your key file
npm install tsx @turnkey/api-key-stamper
export TM_KEY_FILE=path/to/your-api-key.json

1. Check you can reach the API​

This call needs no key. It returns every asset you can trade. The script finds SOL on Solana in this list.

Find SOL in the asset catalog, no key needed
const { data: assets } = await get("/v1/gateway/assets");
const sol = assets.find(
(a: any) => a.symbol === "SOL" && a.chain === "solana",
);
Response200
{
"data": [
{
"id": "495e07ac-fa3e-4179-85b7-b1e8cece3dc4",
"symbol": "SOL",
"chain": "solana",
"venue": "defi",
"…": "…"
}
]
}

2. Mint a token​

The script signs the string {key_id}.{timestamp} with your key file and exchanges it for a token. The timestamp is Unix seconds and must be within 30 seconds of ours. Send access_token in Authorization: Bearer on every call. It lasts an hour, and refresh_token gets you a new pair without signing again.

The same key file signs your orders later, so the script turns it into the hex keys the Turnkey stamper takes.

Load your key file for signing
// 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;
Sign the challenge and mint the token
const timestamp = Math.floor(Date.now() / 1000);
const signature = sign(
"sha256",
Buffer.from(`${key_id}.${timestamp}`),
{
key: createPrivateKey({ key: jwk, format: "jwk" }),
// r and s concatenated, which the API expects
dsaEncoding: "ieee-p1363",
},
).toString("base64url");

const minted = await post("/v1/auth/api-key/token", {
key_id,
timestamp,
signature,
});
token = minted.access_token;
// minted.refresh_token gets a new pair later, without signing.
Response200keep both tokens
{
"access_token": "eyJhbGciOi…",
"refresh_token": "eyJhbGciOi…",
"expires_in": "2026-10-01T18:11:27Z",
"token_type": "Bearer"
}

3. Fund your wallet​

The script reads your balances and waits until your Solana wallet holds enough USDC. Send about 3 USDC to it on Solana; Funding shows where to find the address. The order spends 2 of it, and we pay the network fees.

Send it on Solana

USDC sent on any other network won't reach this wallet.

Wait until your wallet holds enough USDC
const usdcAvailable = async () => {
const { data } = await get("/v1/gateway/balances");
const usdc = data.find(
(b: any) => b.symbol === "USDC" && b.chain === "solana",
);
return Number(usdc?.available ?? 0);
};
if ((await usdcAvailable()) < Number(ORDER_USDC)) {
console.log("… send about 3 USDC on Solana to your wallet");
while ((await usdcAvailable()) < Number(ORDER_USDC)) {
await sleep(10_000);
}
}

4. Place the order and execute​

The script places a market buy for 2 USDC of SOL. The response carries the quote and the payloads, the unsigned wallet transactions to sign. The script signs each one with your key and executes right away. auth_type: "api_key" says your key made the signatures, rather than a passkey.

A quote lasts about two minutes, so doing it in one go keeps it from expiring. If execute still returns quote_stale, run the script again.

Execute usually returns complete. A buy that first moves funds between chains returns pending, and the script reads the order until it's final. If order_id comes back empty, no order was created, and quote.issues says why.

Place the order, sign the payloads, execute
const order = await post("/v1/gateway/orders", {
asset_id: sol.id,
qty: ORDER_USDC,
qty_unit: "quote",
side: "buy",
type: "market",
});
// An empty order_id means no order was created, and
// quote.issues says why.
if (!order.order_id) {
throw new Error(
`no order: ${JSON.stringify(order.quote?.issues)}`,
);
}

// Sign each payload and execute straight away, before the
// quote expires.
const signatures = await Promise.all(
order.payloads.map((p: any) => stamp(p.payload)),
);
const executed = await post(
`/v1/gateway/orders/${order.order_id}/execute`,
{ signatures, auth_type: "api_key" },
);
Response200from execute
{ "status": "complete" }

5. See the fill​

The script reads the order back, then finds the same fill in your transactions. The row's status reads completed, the word the transactions list uses for anything finished.

Read the order, then find it in your transactions
let filled = executed;
while (
!["complete", "canceled", "failed"].includes(filled.status)
) {
await sleep(2000);
filled = await get(`/v1/gateway/orders/${order.order_id}`);
}
console.log(
`✓ order ${filled.status} ` +
`${filled.executed_qty ?? ""} SOL for ${ORDER_USDC} USDC`,
);

const feed = await get("/v1/gateway/transactions?type=order");
const row = feed.data.find((t: any) => t.id === order.order_id);

Run it​

Run the script. You can run it again: each run buys another 2 USDC of SOL while your wallet has funds.

Run the script
npx tsx quickstart.ts
Example output
✓ token expires 2026-10-01T18:11:27Z
… send about 3 USDC on Solana to your wallet
✓ funded
✓ order complete 0.0102 SOL for 2 USDC
✓ in your transactions (completed)

Where to go next​

Every error response includes a request_id. Include it when you email [email protected], so we can find the request.