VEEMANDocs
DocumentationSupportOpen Dashboard

Getting started

  • Introduction
  • Quick start
  • Core concepts

Importing data

  • Supported formats
  • How import works
  • Splitting a file
  • Troubleshooting

Building your portfolio

  • Workspace & equity
  • Weights & capital
  • Weekday filter
  • Keyframes
  • Currencies & FX
  • Library
  • Strike guard
  • Variants & comparison
  • History (undo/redo)
  • Shared spaces

Analytics

  • Metrics reference
  • Charts (deep-dive)
  • Monthly P&L
  • Correlations
  • Comparison
  • Monte Carlo
  • Equity Control
  • Optimization
  • IBKR reconciliation
  • Veeman AI

Research

  • Research overview
  • Define
  • Composing a backtest
  • How the engine trades
  • Results & archive
  • Parameter study
  • Rewind
  • Tactics
  • Indicators

Reference

  • Keyboard & command palette
  • Exporting data
  • Settings

Technical

  • Architecture
  • API reference
  • Security
  • Your data & privacy
  • Risk disclosure
← PreviousArchitectureNext →Security

API reference

VEEMAN's frontend talks to a FastAPI backend over a REST API. This page documents that surface for transparency.

Internal API

This API exists to serve the VEEMAN web app. It is authentication-gated, scoped to your user, and not a public, versioned contract — endpoints may change without notice. There is no public API key programme.

Authentication

Every request carries a Supabase JWT as Authorization: Bearer <token> (or, in local development, a backend-signed cookie). The backend verifies it and resolves your user_id; unauthenticated requests are rejected. See Security.

Auth

POST/api/auth/login
Local-dev login; sets the session cookie.
POST/api/auth/logout
Clears the session.
GET/api/auth/me
The current identity.

Sessions & strategies

POST/api/analysis/upload
Upload one or more CSVs; creates a session.
GET/api/analysis/sessions
List your sessions.
GET/api/analysis/session/{sid}
Session detail (strategies, parameters, per-strategy sizing).
PATCH/api/analysis/session/{sid}
Update parameters (capital, mode, max daily risk, compounding). base_currency restates the session in another currency — every amount, the capital and the manual costs included, scaled by one rate, with ratios and percentages untouched; refresh_fx re-reads the rate fixing the session had pinned; fx_mode (spot|historical) chooses whether foreign legs convert at today's rate or at each day's own — a property of the portfolio, saved with it, and reported back as fx_mode_applied (which falls back to spot when the rate history is unavailable). See Currencies & FX.
PUT/api/analysis/session/{sid}/strategies
Update weights / visibility / per-strategy sizing (mode, SL, Cap %, min/max), the source currency of a leg (single or several at once), and/or the arrangement — the optional order field lists the strategy names from first row to last.
PATCH/api/analysis/session/{sid}/strategies/{name}/rename
Rename a strategy in the session ({new_name}, up to 120 characters).
DELETE/api/analysis/session/{sid}/strategies/{name}
Remove a strategy from the session.
POST/api/analysis/session/{sid}/strategies/delete
Remove several strategies at once — names lists them. Deleting a selection is one gesture: one call, one Timeline entry (so a single undo brings them all back), one recomputation. Unknown or repeated names are ignored; an empty list is a no-op. Groups left without members are pruned, as with the single delete.
GET/api/analysis/session/{sid}/strike-guard
The strike guard state of the session's strategies that can be checked, in rail order: Veeman backtests (kind: "backtest", file = the archived backtest) and strategies imported from a file whose trade log lists the legs (kind: "file", file: "file:…" = the file's fingerprint). Collisions between their original trades (per strategy: entries, days, with whom, examples), each strategy's rule, the trades the portfolio doesn't take (untraded: indices of the backtest's trades, or rows of the file, the trade log's ref), and a status — none, clear, needs_rules, applied (the series were re-run for this exact composition) or stale (the composition changed since).
POST/api/analysis/session/{sid}/strike-guard/apply
Apply a strike guard run written by Research: {run_id}. The re-run strategies replace the originals in the session (the archived backtests don't change); an imported strategy keeps its file, with the skipped and re-priced trades layered on top. 409 if the portfolio changed while the run was going (strategies, rail order, rules, weekdays, or the trades it takes). With run_id: null, when no collision is left, the re-run strategies go back to their original backtests or files. One Timeline entry.
GET/api/analysis/session/{sid}/backtests
The session's Veeman backtests and where each stands against the backtest archive, for Update data: link (current = the strategy holds the run in the archive, moved = the backtest was updated and current_file is the newer run, missing = deleted), the strategy's dates (series) and, for the archived run, how far it goes (archive.through) and whether its recipe ends on purpose before the data (archive.fixed). How far the market data goes comes from GET /api/research/data/status.
POST/api/analysis/session/{sid}/backtests/sync
Move Veeman strategies onto the current run of their backtest: {uids?, moves?} (all of them without uids; moves = {old file: new file} from an update just made, checked against the run's lineage). Weights, sizing, costs, weekdays, groups and keyframes stay; a strike guard rule stays but its re-run is dropped. The session is marked unsaved. Returns synced, current, missing, the session and the new state.
POST/api/analysis/session/{sid}/strike-guard/revert
Every strategy back to its archived backtest or its file, rules cleared. One Timeline entry.
POST/api/analysis/session/{sid}/strike-guard/input
The trades of the strategies imported from a file for a strike guard run: their file's rows on the days with a collision, written to your storage. Returns {input} (null without imported strategies), to pass to POST /api/research/strike-guard, which reads it and deletes it.
POST/api/analysis/session/{sid}/external-trades
The trades of strategies imported from a file, translated for Rewind: {items: [{uid, refs: [int]}]}, where refs are the ref of the trade log rows (the row of the trade in its file). Returns, per strategy, each trade either as a replayable row (legs, entry and exit times, exit reason, contracts, the file's P&L and costs, the file's SPX and VIX) or with the reason it can't be replayed. Rewind sends the rows on to the Research routes below; nothing is saved. The trade log (GET …/allocation) already says which rows can't be replayed, in external. With the strike guard applied the rows are the portfolio's (a skipped trade can't be replayed, a shifted one carries the moved structure and its estimated P&L); original: true returns the file's rows instead.
GET/api/analysis/session/{sid}/keyframes
The session's dated allocation events, sorted by date.
POST/api/analysis/session/{sid}/keyframes
Add one — date, scope (strategy | group | portfolio plus a target), override (contracts, strategy scope only, or enabled) and an optional note. The server assigns the id and keeps the list in date order. 422 if the target is unknown, the date malformed, or contracts is asked outside strategy scope.
PUT/api/analysis/session/{sid}/keyframes/{kfid}
Replace one in place; the id lives in the path and the body cannot change it. PATCH is an alias with the same semantics. 404 if unknown.
DELETE/api/analysis/session/{sid}/keyframes/{kfid}
Remove one. Each of these three is a single Timeline entry, so one undo takes it back.
PUT/api/analysis/session/{sid}/groups/order
Arrangement of the group frames in the rail: order lists the group ids from first to last. It moves whole sleeves and never touches the strategies' own order. Tolerant: unknown ids are ignored, groups you leave out keep their current relative position at the end.
POST/api/analysis/session/{sid}/groups/{gid}/merge
Merge a sleeve into one independent strategy carrying its daily P/L as reported (weights, group size, sizing, costs and weekdays already applied). name defaults to the group's name; consolidate: false keeps the group and adds the new leg muted (active it would count the same P/L twice), consolidate: true removes the merged members and activates the new leg. Only visible members are merged — muted ones are never deleted. One Timeline entry for the whole gesture. 422 if the group is unknown or has nothing active.
DELETE/api/analysis/session/{sid}
Delete the session — and its draft.

Session drafts (autosave)

Every session is mirrored to a draft continuously; GET /session/{sid} rebuilds a session from its draft automatically when it is no longer in memory, so a client that keeps its session id needs none of these. They exist for the case where that id is gone (another browser, cleared storage). See Core concepts.

GET/api/analysis/session/{sid}/draft
Timestamp of the last autosave (saved_at); also returned as draft_saved_at on the session payload.
GET/api/analysis/drafts
Your recoverable drafts, newest first (summaries only — no series). Drafts whose data record is unreadable are omitted rather than listed as unopenable.
POST/api/analysis/drafts/{sid}/resume
Restore a draft and return the session (same session_id).
DELETE/api/analysis/drafts/{sid}
Discard a draft, and its session if still live.

404 and 503 mean different things on a session route. 404 is a verdict: no such session, and no draft to rebuild it from — a client holding that id should drop it. 503 (with Retry-After) means the opposite: the draft exists, but storage could not be reached just now. Keep the session id and retry — discarding it on a 503 would throw away the only pointer to work that is still there. A single failed read is retried internally, so a 503 only surfaces when storage is unavailable for longer than one attempt.

Restoring a draft never counts against the per-user session limit: reopening your own work is not the same as starting something new.

Analytics

GET/api/analysis/session/{sid}/equity
Aggregated equity curve (with sizing flags/warnings when dynamic sizing is active). In dollars mode each series is sent once: the absolute-dollar arrays (equity_abs) would repeat equity, so they arrive as null with equity_abs_omitted: true and the client reuses equity.
GET/api/analysis/session/{sid}/metrics
Performance & risk metrics. Optional window_from/window_to (inclusive ISO days) + rebase scope them to the framed period.
GET/api/analysis/session/{sid}/rolling
Moving-window return distributions (1w→1y) of the current portfolio.
GET/api/analysis/session/{sid}/allocation
Per-trade allocation rows of the sized legs (empty when all strategies are fixed). backtests maps the strategies born from a backtest to their archive file; external gives, per strategy imported from a file, the rows Rewind can't replay (skip: ref → code, all for the whole file, why: code → message) and, with the strike guard applied, the trades it skips (gs: ref → day) and the ones it shifted with an estimated P&L (est). Why each trade closed travels in the header, not on the rows: exit_labels is a list of [code, file text] pairs (index 0 = no reason) and exits maps each strategy whose source records exit reasons to one index into exit_labels per trade, by ref.
GET/api/analysis/session/{sid}/fx-effect
How much the exchange rate gave or took: total, share of net, cumulative curve, per-strategy breakdown, forward-looking exposure per currency with ±10% scenarios, and risk — the same metrics on both readings side by side. alert is raised when the drift makes the constant-rate reading misstate the foreign part by more than ~10%. Over the same window_from/window_to as the metrics. available: false when every strategy is already in the reading currency. Assumes each day is converted at that day's rate; sizing is not re-simulated.
GET/api/analysis/session/{sid}/monthly
Monthly P/L grid. Takes the same window_from/window_to/rebase as the metrics, so the grid and the ledger describe the same period.
GET/api/analysis/session/{sid}/daily
The portfolio's daily P/L, the series behind the monthly cells, with the same window parameters ({rows: [{date, pnl}], currency, signature}). /api/analysis/variants/{vid}/daily gives a variant's. The browser sends it to POST /api/research/events/breakdown with fill: true for the economic events analysis.
GET/api/analysis/session/{sid}/strategies/metrics
Per-strategy register (standard set + trade-level options metrics). Same window_from/window_to/rebase: each leg is clipped and re-anchored with its own factor, returned alongside as window_factors.
GET/api/analysis/session/{sid}/weekday/{name}/stats
Per-weekday statistics of one strategy. Optional window_from/window_to clip them to the framed period (no rebase: one-contract dollars).
GET/api/analysis/session/{sid}/weekday/{name}/series
Per-weekday cumulative equity and drawdown. Same window parameters; inside a window the curves restart from zero on its first day.
POST/api/analysis/session/{sid}/montecarlo
Run a Monte Carlo simulation (with dynamic sizing active it re-runs the allocation per path; block_size weekly/monthly/quarterly, block bootstrap only). The response keeps the model's standing caveat (fixed contracts or compounding) in method_note, apart from warnings, which lists only situational, actionable notices (horizon beyond the history, winsorizing on, a poorly fitting model).
POST/api/analysis/session/{sid}/equity-control
Start an Equity Control diagnosis — returns a job_id.
GET/api/analysis/session/{sid}/equity-control/{job_id}
Poll an Equity Control job (running / done / error).
POST/api/analysis/session/{sid}/walkforward
Start the walk-forward weekday matrix (one Equity Control diagnosis per strategy × weekday cell) — returns a job_id. Legacy module: retired from the UI, still served for direct callers.
GET/api/analysis/session/{sid}/walkforward/{job_id}
Poll a walk-forward job (running / done / error). Legacy.
POST/api/analysis/session/{sid}/correlation
Correlation matrix.
POST/api/analysis/session/{sid}/compare/correlation
Pairwise correlations for the Comparison section. subjects (2 to 8) mixes the current portfolio ({kind: "portfolio"}), saved variants ({kind: "variant", id}) and session strategies ({kind: "strategy", name}); optional inclusive start_date/end_date. Returns matrix in the order of the subjects, from the same engine as the correlation matrix (for two visible strategies, the very cell); a null cell means fewer than 10 trading days in common, or a subject without data.
GET/api/analysis/variants
Variant summaries. base_currency restates them into the currency you are reading in — one rate on the money, nothing else — so a currency switch doesn't break the comparison. Same parameter on /variants/{vid} and /variants/{vid}/window. When the rate is unreachable the variant keeps its own currency and the payload says currency_unconvertible.
POST/api/reconcile/upload
Reconcile an Option Omega trade log (oo_csv) with an IBKR Activity Statement (ibkr_csv): structures, matched executions, daily P/L, equity lines and summary. Optional form field qty_overrides, a JSON object {"oo-3": 2} that forces the contract scale of a structure (whole number 1 to 10; omit a key for automatic detection). Each structure reports scale, scale_source, scale_status and observed_ratios. See IBKR reconciliation.
GET/api/analysis/fx
Current exchange-rate listing: rates, the fixing date, the source and whether it is stale. The frontend never calls the rate provider itself, so the date it shows next to a converted figure is the one the backend converted with. refresh=true forces a re-read.

Session history (undo / redo / recall)

GET/api/analysis/session/{sid}/timeline
The change list of the session: nodes (newest first), the live path, cursor and undo/redo flags. Each node carries the metrics of that version over the full history.
GET/api/analysis/session/{sid}/timeline/window
The same nodes' metrics over the framed period ({node_id: metrics}), each recomputed from its own recipe with the engine that computes the live session. Takes the same window_from/window_to/rebase as the metrics; {} without a window, where the frozen numbers already are the view.
POST/api/analysis/session/{sid}/timeline/undo
Step back one change (also /redo) — rewrites the live session and returns it with the new history state.
POST/api/analysis/session/{sid}/timeline/recall
Jump to any node by seq or node_id, abandoned branches included.

History is in-memory and volatile by design — see History.

Library, variants & risk sizing

GET/api/analysis/portfolio/saved
Saved portfolios (also POST / DELETE).
GET/api/analysis/strategies/saved
Saved strategies (also POST / DELETE).
GET/api/analysis/variants
List / freeze / restore variants.
POST/api/analysis/variants/{vid}/scratch
Open a variant's working copy — a real session you then edit with the normal /session/{sid}/… routes (also DELETE to close it).
POST/api/analysis/variants/{vid}/commit
Re-freeze the variant from its working copy: recipe, equity, drawdown and metrics recomputed, identity unchanged.
GET/api/analysis/variants/{vid}/window
Metrics + curves of a variant over the framed period, computed with the same per-leg recomposition as the live session, plus n_trades: the trades of its visible strategies on that period.
POST/api/analysis/proporzionatore/run
Start a Risk Sizing run — returns a job_id.
POST/api/analysis/proporzionatore/montecarlo
Start a Risk Sizing Monte Carlo — returns a job_id.
GET/api/analysis/proporzionatore/jobs/{job_id}
Poll a Risk Sizing job (running / done / error).

Naming

In the codebase and API these endpoints are still called proporzionatore. They power the standalone Risk Sizing engine, retained as legacy — the same risk/margin engine now runs inline in the workspace as per-strategy sizing.

Research

Mounted only where the Research section is enabled, under the same authentication. Research may run on its own backend; the routes that touch an Analysis session or the AI keys live under /api/analysis.

Data and universe

GET/api/research/universe
Underlyings and expirations (DTE) available: the source of every underlying and DTE selector.
GET/api/research/data/status
Coverage of the dataset: available, days, first and last day, dataset version, VIX coverage.
GET/api/research/days
Trading days in the dataset.
GET/api/research/events/catalog
The economic event types (FOMC, CPI…) with their group, usual release time (ET), the dates known (coverage) and the next scheduled date; plus the calendar version and its last automatic update. Without a calendar on the server: available: false.
POST/api/research/events/preview
Economic event rules ({rules, close, start, end}: the same entry.event_rules and exit.event_close as a recipe) resolved on the period before a run: sessions of the dataset, sessions that pass, sessions each rule hits (for a window, the sessions it pauses), the run's warnings, the sessions skipped or let through, the windows (paused) and, with close, the sessions it would close early on (close_days). 422 when a rule names an event with no dates loaded.
POST/api/research/events/breakdown
A backtest's P&L on the sessions of each event type against the other sessions ({daily, offset}: the daily rows with date, pnl, traded; offset −1, 0 or 1). Per type: sessions, average and win rate per traded session for the event and the others, the difference with its 90% band and verdict (worse, better, same, few), the what-if of skipping (pnl_delta, Sharpe change and band); plus the check for chance across types (family). With fill: true the trading sessions missing between the first and the last row enter at zero, without trades: for a series that lists only the days with trades, like an Analysis portfolio's (traded may be left out: a non-zero P&L counts as traded).
GET/api/research/events
The events of a period (?start=&end=&kinds=), scheduled and not: date, type, time ET, reference period, and scheduled: false for the unscheduled ones, which never act as a filter.
GET/api/research/expirations
Expirations available on a day (?date=).
GET/api/research/spot
The index price minute by minute for a day: [minute of day, price] pairs.
GET/api/research/chain
Chain snapshot as of date + time (per contract, the last quote not after that time) with greeks; expiration absent = same day. IV and greeks are on the real time to expiry; tte gives the year and the floor in minutes to price them with, settle the settlement time (13:00 on half-days). At the settlement and after, IV is null.
GET/api/research/gex
Gamma and delta exposure of a day (?date=), minute by minute: net, call and put GEX (dollars per 1% move) and DEX, gamma flip, call wall, put wall, per-strike bars (call, put and net GEX, call and put DEX, the open interest, and customers' net contracts when the day has signed trade flow), and the repriced GEX profile. model picks the dealer positioning: classic (open interest, the default) or flow (signed trades of the day; meta.flow_coverage is the share of volume signed). Money values are in millions of dollars (meta.units); meta.expirations lists the expirations included. 404 when the day has no open interest (classic) or no signed flow (flow).
POST/api/research/tape
Intraday series of only the requested contracts (Define's time slider), with IV and greeks on the same real-time convention as the chain.

Backtests

GET/api/research/templates
The default recipe that pre-fills the composer.
POST/api/research/backtest
Validate a recipe and run it — returns a job_id. Errors in the recipe come back as 422 with a readable message. The recipe's tactics is one Tactic reference {set_name, set_hash, rules: []} or a list of up to six: the backend embeds each one's rules and name (label) at submit and refuses a Tactic that changed since set_hash.
GET/api/research/backtest/{job_id}
Poll a backtest job (running / done / error) and get its result. 404 if the job is unknown (for example after a restart).
GET/api/research/saved
Summaries of the saved backtests, with tags, note, the recipe's legs and underlying (for the archive filters), data period and revision (also POST to save {display_name, result, overwrite?}). Names are unique in the archive: a name already in use answers 409 with {code: "name_taken", filename, display_name}; sending that filename as overwrite replaces that backtest: the run is saved as a new file, the old one is deleted (returned as replaced) and its tags, note and hidden mark carry over.
POST/api/research/saved/refresh
Bring saved backtests up to the latest market data (Update data): {files, extend?}. Each backtest re-runs with its own recipe up to the dataset's last day and the run replaces it in the archive like overwrite (new file, the old one deleted, name, tags, note and hidden mark carried over, lineage = the runs it replaced); strategies saved from it in the library follow. A recipe that ends on purpose before the data is left alone unless its file is in extend. Returns a job_id.
GET/api/research/saved/refresh/{job_id}
Poll an update: progress (done, total, name) while running, then dataset_end and one item per backtest with its status (updated with new_file, current, fixed, missing or error), the period before/after, what the new days added (sessions, trades, P&L) and, when the earlier period changed too, revised.
GET/api/research/saved/{filename}
A saved backtest in full (also DELETE).
PATCH/api/research/saved/{filename}
Rename a saved backtest or set its note and tags: {display_name?, note?, tags?}; a name another backtest already has answers 409 (name_taken); an omitted field is left alone, tags: [] clears the tags and an empty note removes the note (plain text, up to 1,000 characters). Inside a shared space send If-Match with the revision from the list.
POST/api/research/saved/{filename}/to-strategy
Write the backtest into the Analysis strategy library (Save as strategy).
GET/api/research/saved/as-strategies
Saved backtests in the shape of library strategies (the Analysis library's projection).
POST/api/research/saved/{filename}/hide
Hide a backtest from the Analysis strategy list (DELETE to undo). The backtest itself stays.
GET/api/research/strategy-recipe/{filename}
The frozen recipe of a library strategy born from a backtest (Reopen in Research).
POST/api/analysis/backtests/{filename}/to-session
Load a saved backtest into an Analysis session.
POST/api/research/strike-guard
Re-run a portfolio's strategies with the strike guard: {legs: [{id, name, file, rule, weekdays, untraded?}], input?} in rail order. A strategy imported from a file has file: "file:…" (as the scan returns it) and its trades come from input, written by the Analysis route above (rule = shift | selective | skip | ignore | null, where ignore keeps the strategy's trades on its own strikes; untraded = indices of the backtest's trades the portfolio doesn't take, or rows of the file for an imported strategy, as the Analysis route above returns them: they hold no strike). Returns a job_id; the result is written to your storage and applied by the Analysis route above. For an imported strategy the result counts the shifted trades with an estimated P&L (stats.estimated) and, in stats.unpriced and a warning, the trades it couldn't shift because their day isn't in Veeman's market data (they stay as in the file).
POST/api/research/replay/what-if
The what-if of a trade's strikes: strikes maps leg ids to the strikes to try (at least one must change), with the trade in one of three forms: {backtest, trade} (a saved backtest and the trade's index in its log), {recipe, trade, pieces?, name?} (a run you haven't saved: its recipe and the log row) or {external: true, trade, name?} (an imported trade, the row from the Analysis route above). A backtest trade is re-run on its day with the recipe: same entry decision, minute and contracts, the recipe's rules on the new strikes. An imported one is re-priced at the file's minutes, an estimate (meta.estimated). Same shape as /api/research/replay, with meta.what_if: edits (leg, from, to) and original (the trade's booked P&L and exit). A strike the market wouldn't fill at the entry, two legs on one contract, or a trade the current data no longer reproduces give a 422 with the reason. When the chosen strikes break an entry filter of the recipe that depends on the structure (premium, short/long ratio, wide spreads, margin sizing), the answer is a 422 with detail: {code: "entry_filters", message, checks} (the rules, in the shape of the entry cause's checks): the same request with accept_filters: true replays it anyway, and meta.what_if.overridden keeps the rules overridden.
POST/api/research/replay/portfolio-day
The whole day of an Analysis portfolio in Rewind: {date, legs: [{id, name, file, rule, weekdays, untraded?}], guard, sizes?}. The earliest position is the replay; every other one (and their Tactics adjustments) comes in portfolio, entries the portfolio doesn't take in portfolio_skipped (skipped by the strike guard, sized at 0 contracts, or off that day). With guard the day is re-run with the portfolio's strike guard rules. sizes maps each strategy id to the multipliers of its trades that day, as the portfolio's trade log has them, in the backtest's order: every position is scaled to its multiplier (units). Without it, the backtests' own sizes. external?: [{id, name, trades: [{trade, units}]}] adds the strategies imported from a file, with their rows from the Analysis route above and the portfolio's size over the file's position; legs can then be empty. They come in portfolio with kind: "external" and a reconcile (the file's P&L against Veeman's mark at the exit); one Rewind can't replay is in portfolio_skipped as unreplayable, with a message and its ref. Each item of legs and external may carry order, its position in the rail: it decides who enters first at the same minute and the strike guard's precedence. An external item may also carry rule, weekdays and untraded (rows of its file, as the scan returns them): with guard, the imported strategies are re-run from the file's original rows (original: true on the Analysis route), shifted trades come back re-priced (replay.meta.estimated) and skipped ones in portfolio_skipped.
GET/api/research/strike-guard/{job_id}
Poll a strike guard run: progress while running, then a summary with the run_id to apply, per-strategy counts and warnings. 404 if the run is unknown (for example after a restart).
POST/api/analysis/ai/compose-backtest
Veeman AI: a description in words → a validated recipe. Never runs anything. Like every Veeman AI route it follows the plan's usage limits: 429 with Retry-After when a quota is used up, 503 when Veeman AI is at capacity for the day, 413 for a body over 4 MB.
POST/api/research/rebuild
Rebuild a recipe from a trade log: multipart file (CSV in the standard layout, with a Legs column, up to 15 MB) and optional strategy. Returns a job_id. The file isn't stored.
GET/api/research/rebuild/{job_id}
Poll a rebuild: progress ({step, run?}) while running, then the result. status: "choose" with strategies when the file holds several strategies (send one as strategy); status: "done" with recipe (validated), fields (each deduced value with evidence, confidence and an equivalent range), verification (identical trades, differences by kind, days only in the file or only on the engine, examples), outliers, unread rows and warnings. 404 if the job is unknown.
POST/api/analysis/ai/explain-rebuild
Veeman AI: a short summary of a rebuild, from its report only ({report}: fields, verification, warnings). Returns {summary, notes}; every number must appear in the report. 422 without an AI provider; same usage limits as the composer.

Parameter studies

GET/api/research/optimize/fields
The parameters a study can vary, with tier, default and unit.
POST/api/research/studies/estimate
Validate a study and estimate its size, mode and duration. Computes nothing.
POST/api/research/studies
Start a study in the background (also GET to list them).
GET/api/research/studies/{sid}
Summary with live progress and, once done, the result (also DELETE).
POST/api/research/studies/{sid}/cancel
Cancel a running or queued study.
POST/api/research/studies/{sid}/resume
Resume an interrupted, cancelled or failed study, reusing the evaluations already saved.
POST/api/research/studies/{sid}/lockbox
Open the lockbox (once per family and period).
GET/api/research/studies/{sid}/recipe
The recipe of a configuration of the study (default: the choice), for Apply to backtest.

Define and Rewind

GET/api/research/models
Saved Define models (also POST to save).
GET/api/research/models/{filename}
One model (also DELETE).
GET/api/research/replay
Replay a trade of a saved backtest (?backtest=&trade=).
POST/api/research/replay
Replay a structure built by hand ({date, entry_time, legs, contracts}), an unsaved trade handed over from the console ({trade, costs, pieces, adjustments}), or a trade of a strategy imported from a file ({trade, external: true, name}, the row from POST /api/analysis/session/{sid}/external-trades): legs, times and P&L from the file, the path on Veeman's data, with meta.external (the file's facts) and meta.reconcile (the file's P&L against Veeman's mark at the exit). A warning and meta.clock_mismatch when the file's SPX at entry is more than 2 points off Veeman's; a warning when the settlement is more than $1 per contract off the file's P&L. For a trade the strike guard shifted, meta.estimated and meta.strike_guard instead of meta.reconcile. In every replay the legs' IV and the greeks are on the real time to the close, declared in meta.tte.
GET/api/research/journal
Saved Rewind journals (also POST to save).
GET/api/research/journal/{filename}
One journal (also DELETE).
GET/api/research/drawings/{underlying}/{date}
Your Rewind drawings for a session day ({list, rev}; an empty list and rev 0 for a day without drawings). They're personal, also inside a shared space.
PUT/api/research/drawings/{underlying}/{date}
Replace the day's drawings ({list, base_rev}): it's written only if base_rev is still the current revision, otherwise 409 with the current drawings in detail.current, to merge and send again. Up to 300 drawings a day (413 above).
GET/api/research/tactics
Saved Tactics (also POST to save from rules or a compact ladder). Each summary has backtest_note: null when the backtest can run the Tactic, otherwise why not (it works in Rewind only). The same field comes with GET on one Tactic.
PUT/api/research/tactics/{filename}
Edit a Tactic in place, keeping its identity (also GET and DELETE).

Paused module

The Signal Lab routes (/api/research/signals…) still exist so that backtests saved with a signal set keep running, but the Signals module is paused and hidden in the app.

Account

GET/api/account/storage
How much your library stores and your plan's limit: used_bytes, limit_bytes (null = no limit).
GET/api/account/export
Download all your data as a .zip archive.
DELETE/api/account
Permanently delete your account and data.

Limits the server enforces

A save that would take your library past its storage limit returns 413, with a readable message and the raw numbers in quota; overwriting, shrinking and deleting always work. A request larger than the upload limit also returns 413. Rate-limited routes (the AI analyst, invites) return 429 with a Retry-After header telling you when to try again: a minute for per-minute limits, until the next day for daily ones. Limits follow your account, not your IP or your current token. Too many requests running at the same time from one account also return 429, with Retry-After: 1. Opening more trade-level data than your sessions may hold at once returns 422 (or a per-file error on upload): close a session and retry. Inside a shared space, a read-only member gets 403 on any change.

Shared spaces

Every route above is scope-aware: send the header X-Veeman-Space: {space_id} and it reads and writes inside that space instead of your personal library. Without the header nothing changes. Only shared kinds follow the header — drafts, settings and jobs stay personal in every case.

GET/api/spaces
Your spaces (also POST to create).
GET/api/spaces/{id}
Roster, invites and your role (also PATCH to rename, DELETE to remove).
POST/api/spaces/{id}/invites
Create an invite (also DELETE /{token} to revoke, POST /{token}/accept to join).
PATCH/api/spaces/{id}/members/{uid}
Change a member's role (also DELETE to remove or leave).
POST/api/spaces/{id}/transfer
Hand ownership to another member.
POST/api/spaces/{id}/copy-in
Copy an object from your library into the space (copy-out for the reverse).
GET/api/spaces/{id}/activity
Saves, restores and roster events, newest first.
GET/api/spaces/{id}/comments/{kind}/{name}
Comment thread on an object (also POST and DELETE); /comment-counts returns them all at once.
POST/api/spaces/{id}/lease/{kind}/{name}
Take or renew the turn on an object (also GET and DELETE).
GET/api/versions/{kind}/{name}
Version history of a shared object.
GET/api/versions/{kind}/{name}/{rev}
One version, with its heavy series recomposed.
POST/api/versions/{kind}/{name}/{rev}/restore
Restore that content as a new version.
GET/api/versions
The space trash: objects deleted but recoverable.

If-Match, or the last writer wins

Inside a space, send If-Match: {rev} with the revision you started from on any update. Without it a late save silently overwrites whoever saved in between. With it the server answers 409 carrying the current revision and its author, so the client can offer Reload or Save as a copy. An optional X-Veeman-Note header becomes that version's message.

Simulations run as background jobs

Both Risk Sizing simulations (the run and its Monte Carlo) execute on the server, decoupled from the HTTP request: the POST returns a job_id immediately and the client polls the jobs endpoint. The run keeps going while your phone screen is off or the tab is in the background, and the recipe is persisted so a mid-run backend restart re-runs it transparently. The session Monte Carlo simulation works the same way.