Overview
Quickstart
Ship a live market-data call in under a minute. One wn_ key unlocks the FIRM data API
(firm.wealthnow.io) and the MCP server, same credential, same credit wallet.
| Time | ~60 seconds to first 200 |
| You need | A Wealthnow workspace and a wn_ key from Overview |
| You get | Authenticated REST access, MCP-ready credentials, shared billing |
Agents & SDKs. The same key works over MCP and generated SDKs, no separate token.
Surfaces
| Surface | Base URL | Auth | Billing |
|---|---|---|---|
| FIRM data API | https://firm.wealthnow.io | Authorization: Bearer wn_ (or X-API-Key) | Flat credits per call |
| MCP server | https://mcp.wealthnow.io/mcp | Sign-in, or the same key | The credits of the route behind each tool |
Most FIRM responses are a JSON envelope: { "ok": true, "timestamp": "<ISO-8601>", …data }, and the product guides note the few that differ. Non-2xx responses do not consume credits.
1. Get your key
- Open Overview in the dashboard and reveal your key (
wn_…), or create one under Developers → API Keys. Copy it once; store it like a password. - Export it for the snippets below:
export WEALTHNOW_API_KEY="wn_..."Keys created before October 2026 start with tengu_ and keep working; TENGU_API_KEY is still read by the SDKs.
2. Authenticate
Prefer the Authorization header (OpenAI/Anthropic SDKs expect it). Authorization wins when both headers are set:
Authorization: Bearer wn_: recommended… X-API-Key: wn_…
Avoid ?api_key= in URLs. Credentials leak into logs, history, and proxies.
curl -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "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:
curl --fail-with-body --silent --show-error --max-time 30 \ -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/market/quote/AAPL"Inspect settlement headers on success: X-Credits-Settlement: consumed, plus cost / remaining when settlement is confirmed.
The quote's market_status comes from the exchange calendar and the New York clock: pre from 04:00 to 09:30 ET, open until 16:00 (13:00 on an early-close day), post until 20:00 (17:00 on an early-close day), and closed otherwise, all day on weekends and exchange holidays. day_volume is the current session's volume only, in whole shares: it is null before 04:00 ET, on weekends and holidays, and when the source's daily bar is empty or from an earlier day. price is the last eligible trade in any session, never an exchange's official-close message. change is the exact difference between price and prev_close (2.485, with no floating-point tail) and change_pct has 6 significant figures. bid, ask and their sizes are served only while the exchange is in a session (pre, open or post) and only when the quote was set during it; otherwise they are null.
/api/market/quote/{ticker} is a supported route that sits outside the OpenAPI spec and the MCP
catalog; it is the one such route these docs use. An unknown ticker is 404 ticker_not_found
on this route; the v3 routes answer unknown_ticker. In the spec, and over MCP as the
fundamentals_ tool, the same question is answered by the price snapshot
(fundamentals, 1 credit, Free). Its fields sit under snapshot; check is_stale before using the price:
curl -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/fundamentals/price_snapshot?ticker=AAPL"# A response captured 2026-09-23 at 00:59 ET (exchange closed), abridged:# {"ok": true, "timestamp": "...", "ticker": "AAPL",# "snapshot": {"ticker": "AAPL", "price": 339.84, "as_of": "2026-09-22T23:59:11Z",# "is_stale": false, "session": "closed", "price_session": "post",# "day_change_percent": 0.253702, "prev_close": 338.98, "prev_close_date": "2026-09-21",# "bid": null, "ask": null, "regular_session": {"date": "2026-09-22", "close": 339.75, ...},# "extended_hours": {"session": "post", "change": 0.09, ...}, ...}}session is the exchange session when the answer is served, and bid/ask are null outside a session. day_change_percent runs from the previous regular close to the last trade, so outside regular hours it includes extended-hours trading; regular_session and extended_hours split the two. See Read the latest price and its freshness.
Pass asset_class=crypto for a crypto asset: without it, a symbol such as BTC returns the US-listed
equity of that name.
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:
curl -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/intel/congress"More routes to try ({ticker} → AAPL). Full catalog: API reference or GET /api/capabilities.
# Live quote: market_data (1 credit)curl -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/market/quote/AAPL" # Price snapshot, the OpenAPI and MCP equivalent: fundamentals (1 credit)curl -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/fundamentals/price_snapshot?ticker=AAPL" # SEC filings: public_filings (1 credit)curl -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/fundamentals/sec_filings?ticker=AAPL&limit=10" # 13F holdings: funds (4 credits, Starter+)curl -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/intel/sec13f?ticker=AAPL" # Private markets search: 5 credits, Pro+ or add-oncurl -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "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 do not retry a 503 that says "retryable": false
or asks for a wait longer than 120 seconds. 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.
import os, time, requests FIRM_BASE = "https://firm.wealthnow.io"KEY = os.environ.get("WEALTHNOW_API_KEY", "")if not KEY.startswith(("wn_", "tengu_")): raise SystemExit("Set WEALTHNOW_API_KEY to your Wealthnow key before making requests.")SESSION = requests.Session()SESSION.headers["Authorization"] = f"Bearer {KEY}" class WealthnowError(RuntimeError): def __init__(self, status, code, message, request_id=None): suffix = f" (request_id={request_id})" if request_id else "" super().__init__(f"[{status} {code}] {message}{suffix}") self.status, self.code = status, code self.message, self.request_id = message, request_id def _string_field(body, name): value = body.get(name) return value if isinstance(value, str) and value else None def _parse_error(resp): """Read flat FIRM errors, route-level 503s, nested legacy errors, and Brain problem+json.""" request_id = resp.headers.get("X-Request-ID") header_code = resp.headers.get("X-Error-Code") try: body = resp.json() except ValueError: return header_code or "unknown", (resp.text or resp.reason or "")[:200], request_id if not isinstance(body, dict): return header_code or "unknown", resp.reason or "", request_id request_id = _string_field(body, "request_id") or request_id detail = body.get("detail") nested = detail if isinstance(detail, dict) else {} # A route-level 503 has no `error`: its code is `status` and its message `reason`. code = (_string_field(nested, "error_code") or _string_field(body, "error_code") or _string_field(body, "error") or _string_field(body, "code") or header_code or _string_field(body, "status") or "unknown") message = (_string_field(nested, "error_message") or _string_field(body, "detail") or _string_field(body, "message") or _string_field(body, "reason") or resp.reason or "") return code, message, request_id def _is_retryable(resp): if resp.status_code in (429, 502): return True if resp.status_code != 503: return False # Most 503s are final (no data exists); retry only the ones FIRM marks. try: body = resp.json() except ValueError: body = None if isinstance(body, dict) and body.get("retryable") is False: return False retry_after = resp.headers.get("Retry-After") if retry_after is not None: # A wait past the 120 s backoff cap means the answer will not change soon. return not retry_after.isdigit() or int(retry_after) <= 120 return isinstance(body, dict) and body.get("retryable") is True def firm_call(path, params=None, method="GET", json_body=None, max_retries=4): """Return JSON or raise WealthnowError. Retry 429, 502, and 503s marked retryable.""" if max_retries < 1: raise ValueError("max_retries must be at least 1") for attempt in range(max_retries): resp = SESSION.request(method, FIRM_BASE + path, params=params, json=json_body, timeout=30) if resp.ok: return resp.json() if attempt < max_retries - 1 and _is_retryable(resp): default_wait = 60 if resp.status_code == 429 else 5 try: wait = max(0, int(resp.headers.get("Retry-After", default_wait))) except ValueError: wait = default_wait time.sleep(min(wait * (2 ** attempt), 120)) continue code, message, request_id = _parse_error(resp) raise WealthnowError(resp.status_code, code, message, request_id) # Market data is included on Free and paid plans; this call consumes one credit.quote = firm_call("/api/market/quote/AAPL")print(quote)Discover endpoints (no key)
curl --fail-with-body --silent --show-error --max-time 30 \ "https://firm.wealthnow.io/api/capabilities" \ | 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 (?shape=firm adds a message field as well):
{ "ok": false, "error": "unauthorized", "detail": "invalid or missing API key. Send your key as 'Authorization: Bearer <key>' (recommended), the 'X-API-Key' header, or a '?api_key=<key>' query param. Get a key + docs at https://app.wealthnow.io/docs/quickstart", "request_id": "example-request-id", "docs": "https://app.wealthnow.io/docs/quickstart", "doc_url": "https://app.wealthnow.io/docs/api-reference#error-responses"}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_. 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 and ?shape=firm 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:
{ "ok": false, "available": false, "status": "unavailable_symbol_volume_curve", "reason": "capture_source_unavailable", "billable": false, "retryable": true}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).
Route-specific 404 codes (options, transcripts, crypto market, datasets) are listed in the
API reference.
| Status | error | Meaning | What to do |
|---|---|---|---|
401 | unauthorized | Key missing, malformed, or not sent at all. | See Troubleshoot a 401. An unset $WEALTHNOW_API_KEY looks identical to a bad key. |
402 | plan_required | Product not in your plan. detail names the product and the plan that unlocks it. | Upgrade. Don't retry. |
402 | usage_exceeded | Wallet empty / overage cap reached. | Top up or upgrade. Don't retry. |
403 | account_suspended | The account behind this key is not active. | Open Billing in the dashboard. A failed payment moves your key to the Free plan, which keeps working with the Free credits until you update your card. |
404 | not_found | Unknown path, or an id that does not exist. | Check the path against GET /api/capabilities. |
404 | unknown_ticker | No listing knows the symbol, current or delisted (v3 routes). param names the parameter. A symbol that could not be checked in time is a 200 with ticker_ instead, not billed; see Unverified symbols. | Fix the symbol; don't retry it. |
404 | ticker_not_found | An unknown ticker is 404 ticker_not_found on this route; the v3 routes answer unknown_ticker. | Fix the symbol; don't retry it. |
404 | unknown_crypto_symbol | /api/crypto/{ticker}: not a pair the feed lists. | Use BTC, BTCUSD or X:BTCUSD; see /api/crypto/universe. |
404 | crypto_ | asset_class=crypto on a route with no crypto model. | Don't fall back to the equity with the same symbol. |
404 | no_analysis_available | fundamentals/ai_analyze has no analysis for this symbol. | Don't retry it. |
422 | validation | A parameter failed validation; detail names it. Some routes send a more specific code. | Fix the request. Don't retry it unchanged. |
422 | unsupported_filter | A news route was sent a filter it does not take (for example ticker where it takes tickers). Nothing was read and nothing billed; detail names the parameter or route to use. | Send the parameter detail names. |
429 | rate_limited | Plan rate limit exceeded. | Wait Retry-After seconds, then retry with backoff. |
429 | billing_unavailable | The metering call didn't confirm a debit, so the request was refused. | Wait Retry-After seconds (5), then retry. |
500 | internal_error | Unexpected server error. | Retry a few times with backoff; report the request_id if it persists. |
502 | unkey_unreachable | Key service blip (transient). | Retry after Retry-After seconds (5). |
502 | billing_unavailable | Credit reservation unavailable (transient). | Retry after Retry-After seconds (5). |
502 | invalid_paid_response | The result wasn't complete JSON within the response size limit. | Retry; if it repeats, narrow the request (smaller limit or date range). |
503 | paid_data_unavailable | The route couldn't produce a complete, usable result. | Don't retry in a loop; try later or with different parameters. |
503 | capability_ | The route is tagged data_pending_v1 and serves no data (retryable: false, status is unavailable_). | Don't retry. |
503 | capability_ | Serving policy temporarily unavailable (retryable: true). | Retry after a few seconds with backoff. |
503 | billing_ | Billing policy temporarily unavailable. | Retry after Retry-After seconds (5). |
503 | status field, no error | A route-level unavailable body (see above). | Retry only when retryable is true. |
503 | upstream_unavailable | The data behind the route could not be read. It is not an empty result; the body has no result field. error_code names the case, such as dataset_unavailable or archive_read_failed (details). | Retry after Retry-After when it is 120 seconds or less. Don't poll when retryable is false or Retry-After is longer, such as 3600 for a dataset that was never built. |
504 | request_timeout | The 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.
-
Check that the environment variable is set without printing it:
: "${WEALTHNOW_API_KEY:?Set WEALTHNOW_API_KEY before making a request}" -
Call
https://firm.wealthnow.iodirectly. Redirects can change which credentials reach the destination, depending on the client and redirect target. -
Send
Authorization: BearerorX-API-Key. For example,-H "Authorization: Bearer $WEALTHNOW_quotes the whole shell argument; quotation marks around the token itself become part of the credential.API_ KEY" -
Check that the key is enabled and unexpired in Developers → API Keys. 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.
: "${WEALTHNOW_API_KEY:?Set WEALTHNOW_API_KEY before making a request}"curl -sS -o /dev/null -w '%{http_code}\n' \ -H "Authorization: Bearer $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/market/quote/AAPL"| Status | Next step |
|---|---|
200 | The key worked for this request. Compare the path and headers with the failing request. |
401 | Check the credential, its delivery, and its enabled/expiry state. |
402 | Read the error code and message for a wallet or entitlement restriction. |
429 | Wait for Retry-After, then retry within the plan's rate limit. |
502 | Retry 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:
| Endpoint | Product | Minimum plan | Credits on success |
|---|---|---|---|
/api/market/quote/AAPL | market_data | Free | 1 |
/api/ | fundamentals | Free | 1 |
/api/snapshot/AAPL | quant_signals | Starter | 3 |
A funded Free key can return 200 for the quote or the price snapshot and 402 plan_required for the
quant 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.
Manage your keys
Every key is listed under Developers → API Keys. All of a workspace's keys draw on the same credit wallet and carry the same plan, so a call costs the same whichever key makes it.
- Primary key. Issued with your plan. It can be rotated but not revoked, so the workspace always has a working key.
- Additional keys. Create one per app, server, or teammate, so each can be rotated or revoked without touching the others. Free workspaces hold up to 10; paid workspaces up to 25.
- Agent connection keys. Each time an agent connects through Sign in to Wealthnow, it gets a key of its own, listed as Agent connection. See Connect via MCP & SDKs. They do not count toward the limits above.
Every workspace member can see the keys, create named keys (with an expiry or a lower limit, if they choose), and rename them. Rotating or revoking a key, and changing an existing key's limit, is for workspace admins.
Last used
The Last used column shows when each key was last presented to the key service: any call that reached key verification, including one that was refused. It comes from verification analytics, which keep 90 days. A key with no call in that window shows Never if it is younger than 90 days, and Not in 90 days otherwise. A dash means the analytics could not be read, not that the key is unused.
Expiry
When you create a key you can choose a date for it to expire. The key stops
working at the end of that day in your time zone, and requests with it get a
401 from then on. Expiry is set at creation only; to extend it, create a new
key. The Expires column shows the date, or Never. An expired key stays
listed, marked Expired, until you revoke it.
Requests per minute
Each key has its own request rate limit, enforced per key on every billed call:
past it, requests get 429 rate_limited until the minute resets. By default a
key has your plan's limit (30 a minute on Free, 120 on Starter, 600 on Pro,
1,200 on Expert, 6,000 on Enterprise). You can set a lower limit for any key
when you create it, or later under Manage. A limit cannot exceed the plan's.
It survives plan changes: after a downgrade it is capped at the new plan's
limit, and clearing the field returns the key to the plan's limit.
Rotate a key
Rotating replaces the key's secret. The new key keeps the name, expiry, and request limit; the old one stops working immediately. The new secret is shown once. Credits are shared, so nothing is lost or re-granted.
- Rotate the primary key with Rotate primary key, and any other key with Rotate key.
- A key GitHub reports as leaked is switched off. Rotate it to get a working replacement. This works while a payment is past due; the replacement then stays off until the invoice is paid.
- An expired key cannot be rotated; create a new one. An agent connection key cannot be rotated either, because its secret lives in the agent's sign-in; revoke it or disconnect the agent instead.
- If a rotation is interrupted, rotate the same key again to finish it; other keys can be rotated meanwhile. If it can no longer finish, it is cancelled: the old key is left as it was and you are asked to try again. If Wealthnow cannot tell whether it finished, you are asked to contact support; nothing is changed meanwhile.
- If the old key cannot be deleted after a rotation, you still get the new key and a warning. The old key is already switched off.
Revoke a key
Revoke key deletes a key for good. Requests with it fail at once. The primary key cannot be revoked; rotate it instead. A key whose rotation has not finished is switched off at once instead; rotate it again to finish, then revoke it.
What a key cannot restrict
Every key can call every product in your plan and add-ons. Keys cannot yet be limited to some products: the data API grants access by plan, not by key.
For Claude Code & AI agents
If you're an agent setting this up for a user, this is all you need:
- Config:
WEALTHNOW_API_KEYfrom env (never hardcode). FIRM basehttps://firm.wealthnow.io. Auth:Authorization: Bearer $WEALTHNOW_. 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.API_ KEY - Discovery:
GET https://(no key needed) lists the live versioned data catalog with params. Read it before generating calls instead of guessing paths.firm.wealthnow.io/ api/ capabilities GET https://is the curated public spec, with each route's lowest plan infirm.wealthnow.io/ api/ openapi.json x-wealthnow-min-plan./api/market/quote/{ticker}, shown above, is a supported route outside both; its spec equivalent is/api/.v3/ fundamentals/ price_ snapshot? ticker= - Responses: response fields differ per endpoint:
/api/market/quotereturnstickerandpriceat the top level, the price snapshot nests them undersnapshot, crypto quotes usequote, and feeds/search may useitemsorresults. Read each endpoint's example before accessing its fields. - Error parsing: the error envelope is flat:
{"ok": false, "error": "<code>", "detail": "<message>", "request_.id": ...} detailis a string. Readingbody["detail"]["error_raises and will make a routine 401 look like a broken API. A route-levelcode"] 503hasstatusandreasoninstead oferroranddetail. 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
429and502(waitRetry-After), and a503only when it carriesRetry-Afteror"retryable": true, with exponential backoff capped at 120s. Never poll a503that says"retryable": falseor carries aRetry-Afterlonger than 120s (adataset_unavailablerefusal for a dataset that was never built sends3600): report that the data is unavailable, not that there are no results. Narrow a504request before retrying it. Do not retry401/402/403/404/422or other503s: 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 (most 1 to 6; the social sentiment search,
/api/v3/research/x_, costs 2,500; see Pricing & credits).sentiment /api/capabilitiesis free. Private Markets search costs 5 credits and requires Pro+ or its add-on; listing/api/v2/datasetscosts 4 credits underfundsand requires Starter+. A403 not_offeredmeans the route is not sold on the API at all (the Copilot). A402 plan_requiredresponse describes the entitlement restriction indetail. - 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/, passv3/ intel/ ml_ prediction/ {ticker} asset_class=equityorasset_class=cryptoexplicitly. A response for one of those nine carries aticker_collisionnote, 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:
# The US-listed EQUITY named BTC (not Bitcoin)curl -H "X-API-Key: $WEALTHNOW_API_KEY" \ "https://firm.wealthnow.io/api/v3/intel/ml_prediction/BTC?asset_class=equity"The response carries an explicit disambiguation note:
{ "ticker_collision": { "note": "This is the US-listed equity 'BTC'. For the crypto asset, pass asset_class=crypto.", "crypto_available": false, "asset_class": "equity" }}Asking for the crypto asset with asset_class=crypto fails closed rather than silently returning the equity. A refusal there means no crypto model covers 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 reference | Every endpoint, plan access, credit costs, and error shapes |
| Pricing & credits | Tiers, add-ons, and per-call costs |
| Connect via MCP & SDKs | Claude, Cursor, and generated clients |