> For the complete documentation index, see [llms.txt](https://docs.forepaas.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.forepaas.org/advanced/code-examples.md).

# Code Examples

Complete reference clients for Node.js and Python with gateway signing, session handling, and end-to-end workflows

This page provides complete, copy-paste-ready reference clients that implement the [gateway signing scheme](/getting-started/authentication.md), manage the user session cookie, and expose a simple `request(method, path, options)` interface used throughout the API reference pages.

{% hint style="info" %}
Set the environment variables `FOREGATE_APP_KEY`, `FOREGATE_APP_SECRET`, and `FOREGATE_API_KEY` before running any example. Credentials are provided by your ForeGate account manager.
{% endhint %}

## Node.js Client

```javascript
// foregate-client.js
import crypto from 'crypto';

const BASE_URL = process.env.FOREGATE_BASE_URL ?? 'https://openapi.foregate.com';

export class ForeGateClient {
  constructor({ appKey, appSecret, apiKey, baseUrl = BASE_URL }) {
    this.appKey = appKey;
    this.appSecret = appSecret;
    this.apiKey = apiKey;
    this.baseUrl = baseUrl;
    this.sessionCookie = null;
  }

  setSessionCookie(cookie) {
    this.sessionCookie = cookie;
  }

  // 'YYYY-MM-DD HH:mm:ss' in UTC
  gatewayDate() {
    return new Date().toISOString().slice(0, 19).replace('T', ' ');
  }

  // Path + query, query keys sorted alphabetically, values NOT URL-encoded
  signedPath(path, query) {
    if (!query || Object.keys(query).length === 0) return path;
    const qs = Object.keys(query)
      .sort()
      .map((k) => `${k}=${query[k]}`)
      .join('&');
    return `${path}?${qs}`;
  }

  sign({ method, contentMd5, contentType, date, signedHeaders, path }) {
    const headerLines = Object.keys(signedHeaders)
      .sort()
      .map((name) => `${name}:${signedHeaders[name]}`);
    const stringToSign = [
      method,
      'application/json',
      contentMd5,
      contentType,
      date,
      ...headerLines,
      path,
    ].join('\n');
    return crypto
      .createHmac('sha256', this.appSecret)
      .update(stringToSign, 'utf8')
      .digest('base64');
  }

  async request(method, path, { query, body, cookieRequired = false } = {}) {
    const hasBody = body !== undefined;
    const rawBody = hasBody ? JSON.stringify(body) : '';
    const contentMd5 = hasBody
      ? crypto.createHash('md5').update(rawBody, 'utf8').digest('base64')
      : '';
    const contentType = hasBody ? 'application/json' : '';
    const date = this.gatewayDate();
    const fullPath = this.signedPath(path, query);

    // REST requests sign only x-api-key (the cookie is sent but not signed)
    const signedHeaders = { 'x-api-key': this.apiKey };
    const signature = this.sign({
      method: method.toUpperCase(),
      contentMd5,
      contentType,
      date,
      signedHeaders,
      path: fullPath,
    });

    const headers = {
      accept: 'application/json',
      date,
      'x-ca-key': this.appKey,
      'x-ca-signature': signature,
      'x-ca-signature-headers': 'x-api-key',
      'x-api-key': this.apiKey,
    };
    if (hasBody) {
      headers['content-type'] = 'application/json';
      headers['content-md5'] = contentMd5;
    }
    if (this.sessionCookie) {
      headers.cookie = this.sessionCookie;
    } else if (cookieRequired) {
      throw new Error(`${path} requires a session cookie — call register() first`);
    }

    const res = await fetch(this.baseUrl + fullPath, {
      method: method.toUpperCase(),
      headers,
      body: hasBody ? rawBody : undefined,
    });

    // Capture the session cookie from Set-Cookie (e.g. on /user/register)
    const setCookie = res.headers.getSetCookie?.() ?? [];
    if (setCookie.length > 0) {
      this.sessionCookie = setCookie.map((c) => c.split(';')[0]).join('; ');
    }

    const text = await res.text();
    let data;
    try {
      data = JSON.parse(text);
    } catch {
      data = text; // gateway errors like "Invalid Signature" are plain text
    }
    if (!res.ok) {
      const gatewayMsg = res.headers.get('x-ca-error-message');
      const detail = gatewayMsg ? ` (${gatewayMsg})` : '';
      throw new Error(`HTTP ${res.status}${detail}: ${text}`);
    }
    return data;
  }

  // Registers (or re-registers) a user and captures the session cookie
  async register({ marchantUserId, userEmail, merchantId }) {
    return this.request('POST', '/user/register', {
      body: { marchantUserId, userEmail, merchantId },
    });
  }
}
```

{% hint style="warning" %}
**Precision:** market/option IDs can exceed JavaScript's safe-integer range (2⁵³), so send every ID in a JSON body as a string; path/query transport types remain endpoint-specific. Keep fields documented as decimal strings (for example balances and volumes such as `396.878928000000000000` or `0E-18`) as strings or parse them with `decimal.js`/`big.js`. Fields documented as JSON numbers, including position quantities and costs, remain numbers.
{% endhint %}

## Python Client

```python
# foregate_client.py
import base64
import hashlib
import hmac
import json
import os
from datetime import datetime, timezone

import requests

BASE_URL = os.environ.get("FOREGATE_BASE_URL", "https://openapi.foregate.com")


class ForeGateClient:
    def __init__(self, app_key, app_secret, api_key, base_url=BASE_URL):
        self.app_key = app_key
        self.app_secret = app_secret
        self.api_key = api_key
        self.base_url = base_url
        self.session_cookie = None
        self._http = requests.Session()

    def set_session_cookie(self, cookie):
        self.session_cookie = cookie

    @staticmethod
    def gateway_date():
        """'YYYY-MM-DD HH:mm:ss' in UTC."""
        return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")

    @staticmethod
    def signed_path(path, query):
        """Path + query, keys sorted alphabetically, values NOT URL-encoded."""
        if not query:
            return path
        qs = "&".join(f"{k}={query[k]}" for k in sorted(query))
        return f"{path}?{qs}"

    def sign(self, method, content_md5, content_type, date, signed_headers, path):
        header_lines = [f"{k}:{signed_headers[k]}" for k in sorted(signed_headers)]
        string_to_sign = "\n".join(
            [method, "application/json", content_md5, content_type, date]
            + header_lines
            + [path]
        )
        digest = hmac.new(
            self.app_secret.encode(), string_to_sign.encode(), hashlib.sha256
        ).digest()
        return base64.b64encode(digest).decode()

    def request(self, method, path, query=None, body=None, cookie_required=False):
        has_body = body is not None
        raw_body = json.dumps(body, separators=(",", ":")) if has_body else ""
        content_md5 = (
            base64.b64encode(hashlib.md5(raw_body.encode()).digest()).decode()
            if has_body
            else ""
        )
        content_type = "application/json" if has_body else ""
        date = self.gateway_date()
        full_path = self.signed_path(path, query)

        # REST requests sign only x-api-key (the cookie is sent but not signed)
        signature = self.sign(
            method.upper(), content_md5, content_type, date,
            {"x-api-key": self.api_key}, full_path,
        )

        headers = {
            "accept": "application/json",
            "date": date,
            "x-ca-key": self.app_key,
            "x-ca-signature": signature,
            "x-ca-signature-headers": "x-api-key",
            "x-api-key": self.api_key,
        }
        if has_body:
            headers["content-type"] = "application/json"
            headers["content-md5"] = content_md5
        if self.session_cookie:
            headers["cookie"] = self.session_cookie
        elif cookie_required:
            raise RuntimeError(f"{path} requires a session cookie - call register() first")

        res = self._http.request(
            method.upper(), self.base_url + full_path,
            headers=headers, data=raw_body if has_body else None,
        )

        # Capture the session cookie from Set-Cookie (e.g. on /user/register)
        if res.cookies:
            self.session_cookie = "; ".join(f"{c.name}={c.value}" for c in res.cookies)

        if not res.ok:
            gateway_msg = res.headers.get("x-ca-error-message", "")
            raise RuntimeError(f"HTTP {res.status_code} ({gateway_msg}): {res.text}")
        try:
            return res.json()
        except ValueError:
            return res.text

    def register(self, marchant_user_id, user_email=None, merchant_id=None):
        body = {"marchantUserId": marchant_user_id}
        if user_email:
            body["userEmail"] = user_email
        if merchant_id:
            body["merchantId"] = merchant_id
        return self.request("POST", "/user/register", body=body)
```

## End-to-End Workflow

Register a user, inspect the account, discover a market, place and manage an order:

{% tabs %}
{% tab title="Node.js" %}

```javascript
import { ForeGateClient } from './foregate-client.js';

const client = new ForeGateClient({
  appKey: process.env.FOREGATE_APP_KEY,
  appSecret: process.env.FOREGATE_APP_SECRET,
  apiKey: process.env.FOREGATE_API_KEY,
});

// 1. Register the user — the session cookie is captured automatically
const user = await client.register({
  marchantUserId: 'mer_ciismd',
  userEmail: 'userciismd@gmail.com',
  merchantId: '101001',
});
console.log('userId:', user.data.userId, 'userName:', user.data.userName);

// 2. Check balances and positions
const assets = await client.request('GET', '/account/assets');
const positions = await client.request('GET', '/account/positions');

// 3. Discover markets
const markets = await client.request('GET', '/markets/list', {
  query: { page: 1, pageSize: 10 },
});

// 4. Read the CLOB before quoting
const price = await client.request('GET', '/orderbook/price', {
  query: { marketId: '2531', optionId: '9725' },
});
console.log('best ask:', price.data.bestAskPrice, 'best bid:', price.data.bestBidPrice);

// 5. Place a LIMIT buy order
const order = await client.request('POST', '/ordering/create-order', {
  body: {
    marketId: 2561,
    outcomeId: 4310,
    optionId: 9935,
    chooseOutcome: 'Yes / No',
    direction: 'BUY',
    orderType: 'LIMIT',
    limitPrice: 0.1,
    share: 10,
  },
});

// 6. List open orders (send big IDs as strings)
const open = await client.request('POST', '/ordering/open-orders', {
  body: { marketId: '2561', outcomeId: '4310', page: 1, pageSize: 10 },
});

// 7. Cancel if needed (array even for a single order)
await client.request('POST', '/ordering/cancel-order', {
  body: { orderOpenIds: [1400001111], marketId: 2561, optionId: 9935, outcomeId: 4310 },
});
```

{% endtab %}

{% tab title="Python" %}

```python
import os
from foregate_client import ForeGateClient

client = ForeGateClient(
    app_key=os.environ["FOREGATE_APP_KEY"],
    app_secret=os.environ["FOREGATE_APP_SECRET"],
    api_key=os.environ["FOREGATE_API_KEY"],
)

# 1. Register the user — the session cookie is captured automatically
user = client.register("mer_ciismd", "userciismd@gmail.com", "101001")
print("userId:", user["data"]["userId"], "userName:", user["data"]["userName"])

# 2. Check balances and positions
assets = client.request("GET", "/account/assets")
positions = client.request("GET", "/account/positions")

# 3. Discover markets
markets = client.request("GET", "/markets/list", query={"page": 1, "pageSize": 10})

# 4. Read the CLOB before quoting
price = client.request("GET", "/orderbook/price",
                       query={"marketId": "2531", "optionId": "9725"})
print("best ask:", price["data"]["bestAskPrice"], "best bid:", price["data"]["bestBidPrice"])

# 5. Place a LIMIT buy order
order = client.request("POST", "/ordering/create-order", body={
    "marketId": 2561,
    "outcomeId": 4310,
    "optionId": 9935,
    "chooseOutcome": "Yes / No",
    "direction": "BUY",
    "orderType": "LIMIT",
    "limitPrice": 0.1,
    "share": 10,
})

# 6. List open orders (send big IDs as strings)
open_orders = client.request("POST", "/ordering/open-orders", body={
    "marketId": "2561", "outcomeId": "4310", "page": 1, "pageSize": 10,
})

# 7. Cancel if needed (array even for a single order)
client.request("POST", "/ordering/cancel-order", body={
    "orderOpenIds": [1400001111], "marketId": 2561, "optionId": 9935, "outcomeId": 4310,
})
```

{% endtab %}
{% endtabs %}

## WebSocket Connection Helper (Node.js)

WebSocket upgrades are signed like a `GET` request. Market channels sign `x-api-key` only; user channels also sign the session cookie (`x-ca-signature-headers: cookie,x-api-key` — alphabetical order). See the [WebSocket API](/advanced/websocket-api.md) for channel semantics.

```javascript
// foregate-ws.js
import crypto from 'crypto';
import WebSocket from 'ws';

const WS_BASE = process.env.FOREGATE_WS_BASE ?? 'wss://openapi.foregate.com';

export function connectSocket({ appKey, appSecret, apiKey, path, query, cookie }) {
  const date = new Date().toISOString().slice(0, 19).replace('T', ' ');

  // Query keys sorted alphabetically, values NOT URL-encoded
  const qs = Object.keys(query).sort().map((k) => `${k}=${query[k]}`).join('&');
  const fullPath = `${path}?${qs}`;

  const signedHeaders = { 'x-api-key': apiKey };
  if (cookie) signedHeaders.cookie = cookie;
  const headerLines = Object.keys(signedHeaders)
    .sort()
    .map((name) => `${name}:${signedHeaders[name]}`);

  const stringToSign = ['GET', 'application/json', '', '', date, ...headerLines, fullPath].join('\n');
  const signature = crypto.createHmac('sha256', appSecret)
    .update(stringToSign, 'utf8').digest('base64');

  const headers = {
    accept: 'application/json',
    date,
    'x-ca-key': appKey,
    'x-ca-signature': signature,
    'x-ca-signature-headers': Object.keys(signedHeaders).sort().join(','),
    'x-api-key': apiKey,
  };
  if (cookie) headers.cookie = cookie;

  return new WebSocket(WS_BASE + fullPath, { headers });
}

// Market data channel (no cookie)
const ws = connectSocket({
  appKey: process.env.FOREGATE_APP_KEY,
  appSecret: process.env.FOREGATE_APP_SECRET,
  apiKey: process.env.FOREGATE_API_KEY,
  path: '/socket/clob-price-update',
  query: { marketId: '9390', outcomeId: '27566', optionId: '55445' },
});

ws.on('open', () => console.log('connected'));
ws.on('message', (data) => console.log('msg:', data.toString()));
ws.on('close', (code, reason) => console.log('closed', code, reason.toString()));
ws.on('error', (err) => console.error('error', err));
```

{% hint style="info" %}
**Heartbeats:** you never need to send business frames — protocol-level ping/pong keeps the connection alive. Reconnect with backoff on `close`, and re-seed order-book state from the REST snapshot after every reconnect (see [Building a local order book](/advanced/websocket-api.md#building-a-local-order-book-required-reading)).
{% endhint %}

## Debugging Checklist

1. On `400 Invalid Signature`, read the `x-ca-error-message` response header — it contains the server's `StringToSign`. Diff it against yours byte-by-byte.
2. Print your string-to-sign with visible separators before signing (`stringToSign.replace(/\n/g, '\\n')`).
3. GET requests: two empty lines (content-md5, content-type) must be present in the string-to-sign.
4. Header names in signed-header lines and `x-ca-signature-headers` must be **lowercase** and **alphabetically sorted**.
5. The `date` header must be `YYYY-MM-DD HH:mm:ss` in UTC (UTC+0), within \~15 minutes of server time.
6. `401 Invalid Key` (plain text) = wrong/unauthorized APP Key; `{"code":1401,"message":"unauthorized: Missing X-API-KEY header","data":null}` = the business layer wants `x-api-key`; `{"code":1401,"message":"unauthorized: Missing Cookie header","data":null}` = the endpoint needs a session cookie.
