---
title: "Getting started with Gateway"
url: https://docs.truemarkets.co/gateway/quickstart
description: "Go from a new organization to your first filled trade for a user, in about 15 minutes."
---

Docs index: https://docs.truemarkets.co/llms.txt

# Getting started with Gateway

By the end you've created a user, funded their wallet, bought a small amount of SOL for them, and found the fill in your organization's transactions. It takes about 15 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

Sign in to the [developer console](https://console.truemarkets.co/) with your work email, then create your organization and an API key. The console downloads the key file; keep it out of your repo. Use a login that isn't also a personal True Markets trading account, because a trading account can't join an organization.

You also need a signer key. You hold it, and it signs your users' transactions. The script generates one, or the console's Users page can make a test one.

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](https://docs.truemarkets.co/examples/gateway/quickstart.ts) or [quickstart.py](https://docs.truemarkets.co/examples/gateway/quickstart.py), that you run at the end.

-   TypeScript
-   Python

Install, then point at your API key file

```bash
npm install tsx @turnkey/api-key-stamper @turnkey/crypto
export TM_KEY_FILE=path/to/your-api-key.json
```

Install, then point at your API key file

```bash
pip install requests cryptography 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 your users can trade. The script finds SOL on Solana in this list.

-   TypeScript
-   Python
-   curl

Find SOL in the asset catalog, no key needed

```typescript
const { data: assets } = await get("/v1/gateway/assets");
const sol = assets.find(
  (a: any) => a.symbol === "SOL" && a.chain === "solana",
);
```

Find SOL in the asset catalog, no key needed

```python
assets = get("/v1/gateway/assets")["data"]
sol = next(
    a
    for a in assets
    if a["symbol"] == "SOL" and a["chain"] == "solana"
)
```

The asset catalog, no key needed

```bash
curl -s https://api.truemarkets.co/v1/gateway/assets
```

Response200

```json
{
  "data": [
    {
      "id": "495e07ac-fa3e-4179-85b7-b1e8cece3dc4",
      "symbol": "SOL",
      "chain": "solana",
      "venue": "defi",
      "…": "…"
    }
  ]
}
```

## 2\. Mint an organization token

The script signs the string `{key_id}.{timestamp}` with your API key and exchanges it for an organization token. Your servers send it in `Authorization: Bearer` on every call. It lasts an hour and there's no refresh token, so mint a new one before it expires.

The token also names your organization, and the script reads `organization_id` from it. The console's Overview shows the same id.

-   TypeScript
-   Python

Sign the challenge and mint the token

```typescript
const { key_id, private_key } = JSON.parse(
  readFileSync(API_KEY_FILE, "utf8"),
);
const timestamp = Math.floor(Date.now() / 1000);
const signature = sign(
  "sha256",
  Buffer.from(`${key_id}.${timestamp}`),
  {
    key: createPrivateKey({ key: private_key, 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;

// The token names your organization: no id to configure.
const claims = JSON.parse(
  Buffer.from(token.split(".")[1], "base64url").toString(),
);
const organizationId = claims.tm.organization_id;
```

Sign the challenge and mint the token

```python
with open(API_KEY_FILE) as f:
    api_key = json.load(f)
jwk = api_key["private_key"]
private_key = ec.EllipticCurvePrivateNumbers(
    b64url_int(jwk["d"]),
    ec.EllipticCurvePublicNumbers(
        b64url_int(jwk["x"]),
        b64url_int(jwk["y"]),
        ec.SECP256R1(),
    ),
).private_key()
timestamp = int(time.time())
der = private_key.sign(
    f"{api_key['key_id']}.{timestamp}".encode(),
    ec.ECDSA(hashes.SHA256()),
)
# The API expects r and s concatenated.
r, s = decode_dss_signature(der)
signature = (
    base64.urlsafe_b64encode(
        r.to_bytes(32, "big") + s.to_bytes(32, "big")
    )
    .rstrip(b"=")
    .decode()
)

minted = post(
    "/v1/auth/api-key/token",
    {
        "key_id": api_key["key_id"],
        "timestamp": timestamp,
        "signature": signature,
    },
)
token = minted["access_token"]

# The token names your organization: no id to configure.
claims = json.loads(
    base64.urlsafe_b64decode(token.split(".")[1] + "==")
)
organization_id = claims["tm"]["organization_id"]
```

Response200keep access\_token

```json
{
  "access_token": "eyJhbGciOi…",
  "token_type": "Bearer",
  "expires_in": "2026-10-01T18:11:27Z"
}
```

## 3\. Create a user

One call creates the user and their Solana and EVM wallets. `external_ref_id` is your own id for this customer, and `signer_public_key` is the public half of your signer key. Keep `user_id`: it goes in `TM-On-Behalf-Of` on every call for this user.

Sending the same `external_ref_id` again returns the same user, so the script is safe to rerun. A `503` means the wallets aren't ready yet, so the script sends the same request again.

This user only ever signs with the key in `signer-key.json`, so keep that file and treat this user as a test user.

-   TypeScript
-   Python

Load or create your signer key

```typescript
// The signer key signs every wallet transaction for the users you
// register it on. Keep this file: a user can only ever sign with
// the key it was created with.
if (!existsSync(SIGNER_KEY_FILE)) {
  const { publicKey, privateKey } = generateP256KeyPair();
  const file = {
    signer_public_key: publicKey,
    signer_private_key: privateKey,
  };
  writeFileSync(SIGNER_KEY_FILE, JSON.stringify(file), {
    mode: 0o600,
  });
}
const signer = JSON.parse(readFileSync(SIGNER_KEY_FILE, "utf8"));
const stamper = new ApiKeyStamper({
  apiPublicKey: signer.signer_public_key,
  apiPrivateKey: signer.signer_private_key,
});
const stamp = async (payload: string) =>
  (await stamper.stamp(payload)).stampHeaderValue;
```

Load or create your signer key

```python
# The signer key signs every wallet transaction for the users you
# register it on. Keep this file: a user can only ever sign with
# the key it was created with.
if not os.path.exists(SIGNER_KEY_FILE):
    key = ec.generate_private_key(ec.SECP256R1())
    file = {
        "signer_public_key": key.public_key()
        .public_bytes(Encoding.X962, PublicFormat.CompressedPoint)
        .hex(),
        "signer_private_key": key.private_numbers()
        .private_value.to_bytes(32, "big")
        .hex(),
    }
    descriptor = os.open(
        SIGNER_KEY_FILE, os.O_WRONLY | os.O_CREAT, 0o600
    )
    with os.fdopen(descriptor, "w") as f:
        json.dump(file, f)
with open(SIGNER_KEY_FILE) as f:
    signer = json.load(f)
stamper = ApiKeyStamper(
    ApiKeyStamperConfig(
        api_public_key=signer["signer_public_key"],
        api_private_key=signer["signer_private_key"],
    )
)


def stamp(payload):
    return stamper.stamp(payload).stamp_header_value
```

-   TypeScript
-   Python

Create the user and their wallets

```typescript
let user;
for (let attempt = 1; ; attempt++) {
  try {
    user = await post(
      `/v1/account/organizations/${organizationId}/users`,
      {
        external_ref_id: EXTERNAL_REF_ID,
        signer_public_key: signer.signer_public_key,
      },
    );
    if (user.wallets.length) break;
  } catch (err: any) {
    // A 503 means the wallets aren't ready yet. The same
    // request finishes them.
    if (err.status !== 503 || attempt === 5) throw err;
  }
  await sleep(2000);
}
const userId = user.user_id;
const forUser = { "TM-On-Behalf-Of": userId };
const solanaAddress = user.wallets.find(
  (w: any) => w.chain_family === "solana",
).address;
```

Create the user and their wallets

```python
for attempt in range(1, 6):
    try:
        user = post(
            f"/v1/account/organizations/{organization_id}/users",
            {
                "external_ref_id": EXTERNAL_REF_ID,
                "signer_public_key": signer[
                    "signer_public_key"
                ],
            },
        )
        if user["wallets"]:
            break
    except ApiError as err:
        # A 503 means the wallets aren't ready yet. The same
        # request finishes them.
        if err.status != 503 or attempt == 5:
            raise
    time.sleep(2)
user_id = user["user_id"]
for_user = {"TM-On-Behalf-Of": user_id}
solana_address = next(
    w["address"]
    for w in user["wallets"]
    if w["chain_family"] == "solana"
)
```

Response201keep user\_id and the Solana address

```json
{
  "user_id": "9c1e7a52-4d3b-4f8e-a6b7-1c2d3e4f5a6b",
  "external_ref_id": "quickstart_user_1",
  "created_at": "2026-10-01T17:02:11Z",
  "wallets": [
    { "address": "7Gk2…Qm9p", "chain_family": "solana" },
    { "address": "0x4a8f…c21e", "chain_family": "evm" }
  ]
}
```

## 4\. Fund the Solana wallet

The script prints the user's Solana address and waits. Send about 3 USDC to it on Solana. 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.

-   TypeScript
-   Python

Wait until the wallet holds enough USDC

```typescript
const usdcAvailable = async () => {
  const { data } = await get("/v1/gateway/balances", forUser);
  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 ${solanaAddress}`,
  );
  while ((await usdcAvailable()) < Number(ORDER_USDC))
    await sleep(10_000);
}
```

Wait until the wallet holds enough USDC

```python
def usdc_available():
    balances = get("/v1/gateway/balances", for_user)["data"]
    usdc = next(
        (
            b
            for b in balances
            if b["symbol"] == "USDC"
            and b["chain"] == "solana"
        ),
        None,
    )
    return float(usdc["available"]) if usdc else 0.0

if usdc_available() < float(ORDER_USDC):
    print(
        f"… send about 3 USDC on Solana to {solana_address}"
    )
    while usdc_available() < float(ORDER_USDC):
        time.sleep(10)
```

## 5\. 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 signer key and executes right away.

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.

-   TypeScript
-   Python

Place the order, sign the payloads, execute

```typescript
const order = await post(
  "/v1/gateway/orders",
  {
    asset_id: sol.id,
    qty: ORDER_USDC,
    qty_unit: "quote",
    side: "buy",
    type: "market",
  },
  forUser,
);
// 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" },
  forUser,
);
```

Place the order, sign the payloads, execute

```python
order = post(
    "/v1/gateway/orders",
    {
        "asset_id": sol["id"],
        "qty": ORDER_USDC,
        "qty_unit": "quote",
        "side": "buy",
        "type": "market",
    },
    for_user,
)
# An empty order_id means no order was created, and
# quote.issues says why.
if not order.get("order_id"):
    raise RuntimeError(
        f"no order: {order.get('quote', {}).get('issues')}"
    )

# Sign each payload and execute straight away, before the
# quote expires.
signatures = [stamp(p["payload"]) for p in order["payloads"]]
executed = post(
    f"/v1/gateway/orders/{order['order_id']}/execute",
    {
        "signatures": signatures,
        "auth_type": "api_key",
    },
    for_user,
)
```

Response200from execute

```json
{ "status": "complete" }
```

## 6\. See the fill

The script reads the order back for the user, then finds the same fill in your organization's transactions, which list every user's orders and transfers. That call has no `TM-On-Behalf-Of`, because it already covers all your users. The row's `status` reads `completed`, the word the transactions list uses for anything finished.

-   TypeScript
-   Python

Read the order, then find it in your transactions

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

// Your organization's transactions cover every user, so this
// call has no header.
const feed = await get(
  `/v1/gateway/organizations/${organizationId}/transactions` +
    "?type=order",
);
const row = feed.data.find((t: any) => t.id === order.order_id);
```

Read the order, then find it in your transactions

```python
filled = executed
while filled["status"] not in (
    "complete",
    "canceled",
    "failed",
):
    time.sleep(2)
    filled = get(
        f"/v1/gateway/orders/{order['order_id']}", for_user
    )
print(
    f"✓ order     {filled['status']}  "
    f"{filled.get('executed_qty', '')} SOL for {ORDER_USDC} USDC"
)

# Your organization's transactions cover every user, so this
# call has no header.
feed = get(
    f"/v1/gateway/organizations/{organization_id}/transactions"
    "?type=order"
)
row = next(
    (t for t in feed["data"] if t["id"] == order["order_id"]),
    None,
)
```

## Run it

Run the script. You can run it again: it reuses the same user, and each run buys another 2 USDC of SOL while the wallet has funds.

-   TypeScript
-   Python

Run the script

```bash
npx tsx quickstart.ts
```

Run the script

```bash
python quickstart.py
```

Example output

```text
✓ token     expires 2026-10-01T18:11:27Z
✓ user      9c1e7a52-4d3b-4f8e-a6b7-1c2d3e4f5a6b   solana 7Gk2…Qm9p
… send about 3 USDC on Solana to 7Gk2…Qm9p
✓ funded
✓ order     complete  0.0102 SOL for 2 USDC
✓ found in your organization's transactions (completed)
```

## Where to go next

-   [How requests and signing work](https://docs.truemarkets.co/gateway/requests-and-signing): the two keys, what each signs, and signing in any language.
-   [Signer keys](https://docs.truemarkets.co/gateway/signing-keys): decide where the key lives before you create real users.
-   [Place orders for a user](https://docs.truemarkets.co/gateway/place-orders): limit orders, listing and canceling.

Every error response includes a `request_id`. Include it when you email [support@truemarkets.co](mailto:support@truemarkets.co), so we can find the request.