Authentication
API keys, scopes and the request envelope.
TextToQuant exposes a small REST API you can call from any language, plus an
MCP server for AI agents. The REST API and the downloadable MCP server both
authenticate with the same API key and share your billing and plan limits. The hosted MCP
endpoint is different: it uses OAuth (scopes mcp:read / mcp:run / mcp:write), not an API key.
See the MCP page for that flow.
API keys#
Create a key under Account → API keys. It's shown once, so store it safely. You can have up to 5 active keys, and revoking one cuts off access instantly.
Send the key on every request, as either header:
Authorization: Bearer ttq_your_key
# or
x-api-key: ttq_your_keyProgrammatic access requires a plan with API access. Keys are shown once at creation. Store them securely.
Key scopes#
The research surface (parse, backtest, indicators, screener) needs no scope. Every key reaches it, and every key issued before scopes existed still does.
The trading surface is different, because it reads and acts on a live account. Two opt in grants gate it, and both default to off:
| Scope | Grants |
|---|---|
trading:read | Positions, balances, orders, fills, closed trades, P&L, risk and runtime state |
trading:write | The risk reducing controls: pause, cancel, close, set stop/take profit, kill switch |
Choose them when you create a key. An existing key never gains a scope automatically: a key you pasted into a script years ago cannot suddenly read your book because we shipped a feature.
GET /v1/me reports what a key actually has, including whether the plan clears the second gate:
{ "success": true, "data": {
"userId": "…", "scopes": ["trading:read"],
"plan": { "tier": "pro", "liveTrading": true },
"capabilities": { "tradingRead": true, "tradingWrite": false }
} }It means the key is valid and simply was not granted that capability. Reissuing it will not help;
granting the scope will. The response names requiredScope and grantedScopes.
The response envelope#
Every response, success or failure, shares one shape. A 2xx reply is the mirror of an error:
// success
{ "success": true, "data": { /* … */ } }
// list endpoints also add pagination
{ "success": true, "data": { "results": [ /* … */ ] }, "pagination": { "limit": 10, "offset": 0, "total": 1, "hasMore": false } }
// error
{ "success": false, "code": "invalid_api_key", "error": "Invalid or revoked API key. Create a new one at Account → API keys." }The code is stable and safe to switch on; the human readable error text may be reworded. Error
responses also include a requestId. Quote it when contacting support.
Rate limits#
240 requests/minute per IP, and a per account ceiling set by your plan (60/minute on Pro,
120 on Power, 240 on Quant and Enterprise), on top of your plan quotas. Every
response carries standard RateLimit headers (limit, remaining, reset). Pace yourself with them.
Over a limit you get a 429 with code rate_limited.
Trading writes carry an extra ceiling of 30 live account actions per minute per user: they reach a venue, so they are budgeted separately from ordinary reads.
Safe retries (idempotency)#
Send an Idempotency-Key header (or idempotency_key in the body) on POST /v1/backtests. Replaying
the same key with the same body within 24h returns the original run, no second credit. The same
key with a different body returns 409 idempotency_key_reuse.
Error reference#
| HTTP | code | What it means |
|---|---|---|
| 401 | missing_api_key | No key on the request: add the header |
| 401 | invalid_api_key | Key is wrong, revoked, or malformed |
| 403 | feature_not_available | Your plan doesn't include this feature (requiredFeature says which) |
| 403 | insufficient_scope | Trading endpoint, key lacks trading:read/trading:write: grant it on the key |
| 403 | upgrade_required | Trading endpoint, plan has no live trading |
| 503 | plan_unavailable | Trading endpoint, the plan couldn't be resolved: fails closed, retryable |
| 403 | plan_history_limit / plan_timeframe_limit | Date range or timeframe beyond your plan |
| 400 | invalid_query / validation_error | Bad input: the message names what failed. Nothing billed |
| 422 | clarification_needed | Parse hit ambiguous terms: data.clarificationsNeeded lists them |
| 422 | missing_parameters | Parse missing a required field: details.missingFields + suggestions |
| 422 | invalid_strategy | Parsed strategy isn't executable: details.structuralErrors say why |
| 422 | cannot_execute | Pre run validation failed: the message lists failing checks |
| 409 | idempotency_key_reuse | Same key, different body: use a fresh key |
| 413 | WORKLOAD_TOO_LARGE | Backtest exceeds the bar count ceiling |
| 429 | rate_limited | Over the request rate: pause and retry |
| 429 | OVERAGE_* | Billing state gate (pending / overdue / cap reached) |
| 404 | not_found | Unknown endpoint, or an id you don't own |
| 503 | QUEUE_BACKPRESSURE / queue_unavailable | Queue busy or down: retryable, credit refunded |
| 500 | execution_failed | The run failed: nothing saved, metered credit refunded |
| 500 | parse_failed | Parser couldn't read the query: details.suggestions hint a fix |
| 500 | internal_error | Unexpected server error: safe to retry |
Usually not. Backtests finish server side even if the HTTP connection drops, and the credit was
already counted. Wait a moment, then call GET /v1/backtests. The newest row is your run.
Next: the endpoint reference.