Overview

Quickstart

Ship a live market-data call in under a minute. One tengu key unlocks the FIRM data API

Live API

Ship a live market-data call in under a minute. One tengu_ key unlocks the FIRM data API (firm.wealthnow.io) and the Brain agent API (brain.wealthnow.io) — same credential, same credit wallet.

Time~60 seconds to first 200
You needA Wealthnow workspace and a tengu_ key from Overview
You getAuthenticated REST access, MCP-ready credentials, shared billing

Agents & SDKs. The same key works over MCP and generated SDKs — no separate token.

Surfaces#

SurfaceBase URLAuthBilling
FIRM data APIhttps://firm.wealthnow.ioAuthorization: Bearer tengu_… (or X-API-Key)Flat credits per call
Brain agent APIhttps://brain.wealthnow.io/v1Authorization: Bearer tengu_…Credits per token (Enterprise)

Every FIRM response is a JSON envelope: { "ok": true, "timestamp": "<ISO-8601>", …data }. Non-2xx responses do not consume credits.

1. Get your key#

  1. Open Overview in the dashboard and create or reveal your key (tengu_…). Copy it once; store it like a password.
  2. Export it for the snippets below:
export TENGU_API_KEY="tengu_..."

2. Authenticate#

Prefer the Authorization header (OpenAI/Anthropic SDKs expect it). Authorization wins when both headers are set:

  1. Authorization: Bearer tengu_…recommended
  2. X-API-Key: tengu_…

Avoid ?api_key= in URLs — credentials leak into logs, history, and proxies.

Shell
1curl -H "Authorization: Bearer $TENGU_API_KEY" \2  "https://firm.wealthnow.io/api/market/quote/AAPL"

3. First data call (Free plan)#

market_data is included on Free — 1 credit per successful quote:

Shell
1curl --fail-with-body --silent --show-error --max-time 30 \2  -H "Authorization: Bearer $TENGU_API_KEY" \3  "https://firm.wealthnow.io/api/market/quote/AAPL"
Python
1import os, requests2 3r = requests.get(4    "https://firm.wealthnow.io/api/market/quote/AAPL",5    headers={"Authorization": f"Bearer {os.environ['TENGU_API_KEY']}"},6    timeout=30,7)8r.raise_for_status()9data = r.json()  # {"ok": true, "ticker": "AAPL", "price": ...}10print(f"{data['ticker']} @ ${data['price']}")

Inspect settlement headers on success: X-Credits-Settlement: consumed, plus cost / remaining when settlement is confirmed.

4. Higher-signal endpoints#

Congressional trades need Starter+ (2 credits). On Free you get 402 plan_required — that is an entitlement signal, not an auth failure:

Shell
1curl -H "Authorization: Bearer $TENGU_API_KEY" \2  "https://firm.wealthnow.io/api/v3/intel/congress"
Python
1r = requests.get(2    "https://firm.wealthnow.io/api/v3/intel/congress",3    headers={"Authorization": f"Bearer {os.environ['TENGU_API_KEY']}"},4    timeout=30,5)6r.raise_for_status()7data = r.json()  # {"ok": true, "timestamp": ..., "items": [...], "count": N}8print(data["items"][:3])

More routes to try ({ticker}AAPL). Full catalog: API reference or GET /api/capabilities.

Shell
1# Live quote — market_data (1 credit)2curl -H "Authorization: Bearer $TENGU_API_KEY" \3  "https://firm.wealthnow.io/api/market/quote/AAPL"4 5# SEC filings — public_filings (1 credit)6curl -H "Authorization: Bearer $TENGU_API_KEY" \7  "https://firm.wealthnow.io/api/v3/fundamentals/sec_filings?ticker=AAPL&limit=10"8 9# 13F holdings — funds (4 credits, Starter+)10curl -H "Authorization: Bearer $TENGU_API_KEY" \11  "https://firm.wealthnow.io/api/v3/intel/sec13f?ticker=AAPL"12 13# Private markets search — 5 credits, Pro+ or add-on14curl -H "Authorization: Bearer $TENGU_API_KEY" \15  "https://firm.wealthnow.io/api/v3/private_markets/search?q=stripe"

Plan gates live in Pricing & credits. Calling a locked product returns 402 plan_required with no credit debit — upgrade under Plans.

Production client#

These clients retry HTTP 429 and 502 responses, and a 503 only when it carries Retry-After or "retryable": true. They preserve the API's error code, message, and request ID, reading a route-level 503's status and reason when it has no error. Successful response data stays endpoint-specific; validate its fields before using it.

Python
1import os, time, requests2 3FIRM_BASE = "https://firm.wealthnow.io"4KEY = os.environ.get("TENGU_API_KEY", "")5if not KEY.startswith("tengu_"):6    raise SystemExit("Set TENGU_API_KEY to your Wealthnow key before making requests.")7SESSION = requests.Session()8SESSION.headers["Authorization"] = f"Bearer {KEY}"9 10class TenguError(RuntimeError):11    def __init__(self, status, code, message, request_id=None):12        suffix = f" (request_id={request_id})" if request_id else ""13        super().__init__(f"[{status} {code}] {message}{suffix}")14        self.status, self.code = status, code15        self.message, self.request_id = message, request_id16 17def _string_field(body, name):18    value = body.get(name)19    return value if isinstance(value, str) and value else None20 21def _parse_error(resp):22    """Read flat FIRM errors, route-level 503s, nested legacy errors, and Brain problem+json."""23    request_id = resp.headers.get("X-Request-ID")24    header_code = resp.headers.get("X-Error-Code")25    try:26        body = resp.json()27    except ValueError:28        return header_code or "unknown", (resp.text or resp.reason or "")[:200], request_id29    if not isinstance(body, dict):30        return header_code or "unknown", resp.reason or "", request_id31    request_id = _string_field(body, "request_id") or request_id32    detail = body.get("detail")33    nested = detail if isinstance(detail, dict) else {}34    # A route-level 503 has no `error`: its code is `status` and its message `reason`.35    code = (_string_field(nested, "error_code") or _string_field(body, "error_code")36            or _string_field(body, "error")37            or _string_field(body, "code") or header_code38            or _string_field(body, "status") or "unknown")39    message = (_string_field(nested, "error_message") or _string_field(body, "detail")40               or _string_field(body, "message") or _string_field(body, "reason")41               or resp.reason or "")42    return code, message, request_id43 44def _is_retryable(resp):45    if resp.status_code in (429, 502):46        return True47    if resp.status_code != 503:48        return False49    # Most 503s are final (no data exists); retry only the ones FIRM marks.50    if resp.headers.get("Retry-After") is not None:51        return True52    try:53        body = resp.json()54    except ValueError:55        return False56    return isinstance(body, dict) and body.get("retryable") is True57 58def firm_call(path, params=None, method="GET", json_body=None, max_retries=4):59    """Return JSON or raise TenguError. Retry 429, 502, and 503s marked retryable."""60    if max_retries < 1:61        raise ValueError("max_retries must be at least 1")62    for attempt in range(max_retries):63        resp = SESSION.request(method, FIRM_BASE + path, params=params,64                               json=json_body, timeout=30)65        if resp.ok:66            return resp.json()67        if attempt < max_retries - 1 and _is_retryable(resp):68            default_wait = 60 if resp.status_code == 429 else 569            try:70                wait = max(0, int(resp.headers.get("Retry-After", default_wait)))71            except ValueError:72                wait = default_wait73            time.sleep(min(wait * (2 ** attempt), 120))74            continue75        code, message, request_id = _parse_error(resp)76        raise TenguError(resp.status_code, code, message, request_id)77 78# Market data is included on Free and paid plans; this call consumes one credit.79quote = firm_call("/api/market/quote/AAPL")80print(quote)
TypeScript
1const FIRM_BASE = "https://firm.wealthnow.io";2const KEY = process.env.TENGU_API_KEY;3if (!KEY?.startsWith("tengu_")) {4  throw new Error("Set TENGU_API_KEY to your Wealthnow key before making requests.");5}6 7function isRecord(value: unknown): value is Record<string, unknown> {8  return typeof value === "object" && value !== null && !Array.isArray(value);9}10 11function stringField(body: Record<string, unknown>, name: string): string | undefined {12  const value = body[name];13  return typeof value === "string" && value.length > 0 ? value : undefined;14}15 16async function parseError(resp: Response): Promise<{17  code: string;18  message: string;19  requestId?: string;20}> {21  const text = await resp.text();22  let body: unknown;23  let fallbackMessage = resp.statusText;24  try {25    body = JSON.parse(text);26  } catch {27    fallbackMessage = text.slice(0, 200) || resp.statusText;28  }29  const record = isRecord(body) ? body : {};30  const nested = isRecord(record.detail) ? record.detail : {};31  // A route-level 503 has no `error`: its code is `status` and its message `reason`.32  return {33    code: stringField(nested, "error_code") ?? stringField(record, "error_code")34      ?? stringField(record, "error")35      ?? stringField(record, "code") ?? resp.headers.get("X-Error-Code")36      ?? stringField(record, "status") ?? "unknown",37    message: (stringField(nested, "error_message") ?? stringField(record, "detail")38      ?? stringField(record, "message") ?? stringField(record, "reason")) || fallbackMessage,39    requestId: stringField(record, "request_id")40      ?? resp.headers.get("X-Request-ID") ?? undefined,41  };42}43 44async function isRetryable(resp: Response): Promise<boolean> {45  if (resp.status === 429 || resp.status === 502) {46    return true;47  }48  if (resp.status !== 503) {49    return false;50  }51  // Most 503s are final (no data exists); retry only the ones FIRM marks.52  if (resp.headers.has("Retry-After")) {53    return true;54  }55  try {56    const body: unknown = await resp.clone().json();57    return isRecord(body) && body.retryable === true;58  } catch {59    return false;60  }61}62 63export async function firmCall(64  path: string,65  init: RequestInit = {},66  maxRetries = 4,67): Promise<unknown> {68  if (!Number.isInteger(maxRetries) || maxRetries < 1) {69    throw new Error("maxRetries must be a positive integer");70  }71  const headers = new Headers(init.headers);72  headers.set("Authorization", `Bearer ${KEY}`);73  for (let attempt = 0; attempt < maxRetries; attempt++) {74    const resp = await fetch(`${FIRM_BASE}${path}`, { ...init, headers });75    if (resp.ok) {76      const data: unknown = await resp.json();77      return data;78    }79    if (attempt < maxRetries - 1 && (await isRetryable(resp))) {80      const defaultWait = resp.status === 429 ? 60 : 5;81      const parsed = Number(resp.headers.get("Retry-After") ?? defaultWait);82      const wait = Number.isFinite(parsed) && parsed >= 0 ? parsed : defaultWait;83      await new Promise((resolve) => setTimeout(resolve, Math.min(wait * 2 ** attempt, 120) * 1000));84      continue;85    }86    const { code, message, requestId } = await parseError(resp);87    throw new Error(`[${resp.status} ${code}] ${message}${requestId ? ` (request_id=${requestId})` : ""}`);88  }89  throw new Error("exhausted retries");90}

The Brain agent API (Enterprise)#

Brain is an OpenAI/Anthropic-compatible agent that orchestrates FIRM tools for you. Enterprise only. Keys without Brain entitlement receive 403 CHAT_ENTITLEMENT_REQUIRED. On Pro and below, call FIRM REST directly.

Python
1from openai import OpenAI2import os3 4client = OpenAI(api_key=os.environ["TENGU_API_KEY"], base_url="https://brain.wealthnow.io/v1")5 6resp = client.chat.completions.create(7    model="brain",8    messages=[{9        "role": "user",10        "content": "What did Nancy Pelosi trade recently, and how did NVDA move around those dates?",11    }],12)13print(resp.choices[0].message.content)

Brain is stateless per request (send full messages[] to continue), supports stream=true, and also exposes Anthropic's shape at POST /v1/messages. It meters the shared wallet by token.

Discover endpoints (no key)#

Shell
1curl --fail-with-body --silent --show-error --max-time 30 \2  "https://firm.wealthnow.io/api/capabilities" \3  | jq '{manifest_version, tool_count, groups}'

Errors#

FIRM uses a flat error envelope: error is the machine-readable code and detail is the message string (message with ?shape=firm). Brain uses code and detail in problem+json:

JSON
1{2  "ok": false,3  "error": "unauthorized",4  "detail": "invalid or missing API key — send your key as 'Authorization: Bearer <key>' ...",5  "request_id": "example-request-id"6}

The same code is usually also on the X-Error-Code response header, and request_id on X-Request-ID — quote the request_id when you contact support and the exact request can be pulled from the logs.

Do not write body["detail"]["error_code"]. detail is a string here, so indexing into it raises TypeError/AttributeError and turns a clean 401 into a stack trace. Use the _parse_error helper in Production client, which handles this envelope, ?shape=firm, and Brain's problem+json alike.

Some 503 responses come from the route itself and carry status instead of error, with no X-Error-Code header. Read status as the code and reason as the message, and retry only when retryable is true. The production clients do this:

JSON
1{2  "ok": false,3  "available": false,4  "status": "unavailable_symbol_volume_curve",5  "reason": "capture_source_unavailable",6  "billable": false,7  "retryable": true8}

Every non-2xx response is non-billable. A request refused before its credit reservation is never debited; one that fails after it is refunded (X-Credits-Settlement: refunded, X-Credits-Cost: 0).

StatuserrorMeaningWhat to do
401unauthorizedKey missing, malformed, or not sent at all.See Troubleshoot a 401 — an unset $TENGU_API_KEY looks identical to a bad key.
402plan_requiredProduct not in your plan. detail names the product and the plan that unlocks it.Upgrade. Don't retry.
402usage_exceededWallet empty / overage cap reached.Top up or upgrade. Don't retry.
403account_suspendedBilling lapsed.Resolve billing in the dashboard.
404not_foundUnknown route or symbol.Check the path against GET /api/capabilities.
422validationA parameter failed validation; detail names it. Some routes send a more specific code.Fix the request. Don't retry it unchanged.
429rate_limitedPlan rate limit exceeded.Wait Retry-After seconds, then retry with backoff.
429billing_unavailableThe metering call didn't confirm a debit, so the request was refused.Wait Retry-After seconds (5), then retry.
500internal_errorUnexpected server error.Retry a few times with backoff; report the request_id if it persists.
502unkey_unreachableKey service blip (transient).Retry after Retry-After seconds (5).
502billing_unavailableCredit reservation unavailable (transient).Retry after Retry-After seconds (5).
502invalid_paid_responseThe result wasn't complete JSON within the response size limit.Retry; if it repeats, narrow the request (smaller limit or date range).
503paid_data_unavailableThe route couldn't produce a complete, usable result.Don't retry in a loop; try later or with different parameters.
503capability_data_unavailableThe route is tagged data_pending_v1 and serves no data (retryable: false, status is unavailable_data_pending).Don't retry.
503capability_policy_unavailableServing policy temporarily unavailable (retryable: true).Retry after a few seconds with backoff.
503billing_policy_unavailableBilling policy temporarily unavailable.Retry after Retry-After seconds (5).
503status field, no errorA route-level unavailable body (see above).Retry only when retryable is true.
504request_timeoutThe request exceeded the server's time budget.Narrow the request (add limit, or a start and end date) before retrying.

Troubleshoot a 401#

A 401 can mean the key is missing, malformed, invalid, expired, or disabled. Check how the request sends the credential before replacing the key.

  1. Check that the environment variable is set without printing it:

    : "${TENGU_API_KEY:?Set TENGU_API_KEY before making a request}"
  2. Call https://firm.wealthnow.io directly. Redirects can change which credentials reach the destination, depending on the client and redirect target.

  3. Send Authorization: Bearer or X-API-Key. For example, -H "Authorization: Bearer $TENGU_API_KEY" quotes the whole shell argument; quotation marks around the token itself become part of the credential.

  4. Check that the key is enabled and unexpired in Wealthnow API → Overview. Rotate any exposed key even if it still works.

Check a documented endpoint#

Market data is included on every plan. This request consumes one credit on success; it tests authentication without requiring a higher-tier product.

Shell
1: "${TENGU_API_KEY:?Set TENGU_API_KEY before making a request}"2curl -sS -o /dev/null -w '%{http_code}\n' \3  -H "Authorization: Bearer $TENGU_API_KEY" \4  "https://firm.wealthnow.io/api/market/quote/AAPL"
StatusNext step
200The key worked for this request. Compare the path and headers with the failing request.
401Check the credential, its delivery, and its enabled/expiry state.
402Read the error code and message for a wallet or entitlement restriction.
429Wait for Retry-After, then retry within the plan's rate limit.
502Retry after the reported delay; the key service may be unavailable.

Distinguish a quote from a paid snapshot#

The quote and snapshot endpoints have different product entitlements:

EndpointProductMinimum planCredits on success
/api/market/quote/AAPLmarket_dataFree1
/api/snapshot/AAPLquant_signalsStarter3

A funded Free key can return 200 for the quote and 402 plan_required for the snapshot. That 402 concerns product access, not an invalid key. Read the error code and message before changing plans or replacing a credential.

Use GET /api/capabilities to discover routes before calling them. A 401 on a guessed path does not establish that the key is invalid. Your plan and balance are visible in Wealthnow API → Overview; /api/me, /api/usage, and /api/keys/whoami are not documented introspection endpoints. Include the response request ID when contacting support; never include the key.

For Claude Code & AI agents#

If you're an agent setting this up for a user, this is all you need:

  • Config: TENGU_API_KEY from env (never hardcode). FIRM base https://firm.wealthnow.io; Brain base https://brain.wealthnow.io/v1. Auth: Authorization: Bearer $TENGU_API_KEY on both. Assert the variable is non-empty before your first call — an unset var sends an empty Bearer token and produces a 401 that is byte-identical to an invalid key.
  • Discovery: GET https://firm.wealthnow.io/api/capabilities (no key needed) lists the live versioned data catalog with params — read it before generating calls instead of guessing paths. (A few legacy convenience routes like /api/market/quote/{ticker} shown above aren't in the manifest but remain supported.)
  • Responses: response fields differ per endpoint: equity quotes return ticker and price at the top level, crypto quotes use quote, and feeds/search may use items or results. Read each endpoint's example before accessing its fields.
  • Error parsing: the error envelope is flat{"ok": false, "error": "<code>", "detail": "<message>", "request_id": ...}. detail is a string. Reading body["detail"]["error_code"] raises and will make a routine 401 look like a broken API. A route-level 503 has status and reason instead of error and detail. Use the production client's _parse_error.
  • 401 diagnosis: check credential delivery and enabled/expiry state, then use the one-credit check in Check a documented endpoint. Include the request ID in support reports; never include a key.
  • Retry policy: retry 429 and 502 (wait Retry-After), and a 503 only when it carries Retry-After or "retryable": true, with exponential backoff capped at 120s. Narrow a 504 request before retrying it. Do not retry 401/402/403/404/422 or other 503s — surface them to the user (they need a key, an upgrade, a fixed path, or data that is not available).
  • Budget: each FIRM call costs a fixed number of credits by product (1–10; see Pricing & credits). /api/capabilities is free. Private Markets search costs 5 credits and requires Pro+ or its add-on; listing /api/v2/datasets costs 4 credits under funds and requires Starter+. Brain is metered per token, priced at 2,500 credits per $1 of model cost. A 402 plan_required response describes the entitlement restriction in detail.
  • Ticker collisions — never present equity data as crypto. Nine symbols are BOTH a crypto asset and a US-listed equity: BTC, ETH, LINK, LTC, COMP, ARB, NEAR, APT, ATOM. On /api/v1/copilot/score/{ticker} and /api/v3/intel/ml_prediction/{ticker}, pass asset_class=equity or asset_class=crypto explicitly. A response for one of those nine carries a ticker_collision note — surface it to the user, don't strip it. See Ticker collisions below.
  • Use the production client verbatim — it already encodes the envelope, error shapes, and backoff correctly.

Ticker collisions: crypto vs equity#

Nine tickers are ambiguous — they name both a crypto asset and a US-listed equity:

BTC · ETH · LINK · LTC · COMP · ARB · NEAR · APT · ATOM

Scoring and prediction endpoints take an explicit asset_class parameter so the namespace is never guessed:

Shell
1# The US-listed EQUITY named BTC (not Bitcoin)2curl -H "X-API-Key: $TENGU_API_KEY" \3  "https://firm.wealthnow.io/api/v1/copilot/score/BTC?asset_class=equity"

The response carries an explicit disambiguation note:

JSON
1{2  "ticker_collision": {3    "note": "This is the US-listed equity 'BTC'. For the crypto asset, pass asset_class=crypto.",4    "crypto_available": false,5    "asset_class": "equity"6  }7}

Asking for the crypto asset fails closed rather than silently returning the equity:

Shell
1curl -H "X-API-Key: $TENGU_API_KEY" \2  "https://firm.wealthnow.io/api/v1/copilot/score/BTC?asset_class=crypto"3# 404  {"error": "crypto_model_unavailable", ...}

A 404 crypto_model_unavailable means the crypto model does not cover that asset — it does not mean "use the equity instead." Non-colliding tickers like AAPL are unaffected and carry no collision note.

For agents: treat ticker_collision as required context to relay. Presenting the equity BTC as Bitcoin is a correctness failure, not a formatting detail.

Next steps#

Go deeper
API referenceEvery endpoint, plan access, credit costs, and error shapes
Pricing & creditsTiers, add-ons, and per-call costs
Connect via MCP & SDKsClaude, Cursor, and generated clients
Chat parityMake Brain feel like Tengu Chat with optional context