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.
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.
/api/auth/login/api/auth/logout/api/auth/me/api/analysis/upload/api/analysis/sessions/api/analysis/session/{sid}/api/analysis/session/{sid}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./api/analysis/session/{sid}/strategiescurrency 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./api/analysis/session/{sid}/strategies/{name}/rename{new_name}, up to 120 characters)./api/analysis/session/{sid}/strategies/{name}/api/analysis/session/{sid}/strategies/deletenames 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./api/analysis/session/{sid}/strike-guardkind: "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)./api/analysis/session/{sid}/strike-guard/apply{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./api/analysis/session/{sid}/backtestslink (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./api/analysis/session/{sid}/backtests/sync{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./api/analysis/session/{sid}/strike-guard/revert/api/analysis/session/{sid}/strike-guard/input{input} (null without imported strategies), to pass to POST /api/research/strike-guard, which reads it and deletes it./api/analysis/session/{sid}/external-trades{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./api/analysis/session/{sid}/keyframes/api/analysis/session/{sid}/keyframesdate, 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./api/analysis/session/{sid}/keyframes/{kfid}PATCH is an alias with the same semantics. 404 if unknown./api/analysis/session/{sid}/keyframes/{kfid}/api/analysis/session/{sid}/groups/orderorder 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./api/analysis/session/{sid}/groups/{gid}/mergename 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./api/analysis/session/{sid}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.
/api/analysis/session/{sid}/draftsaved_at); also returned as draft_saved_at on the session payload./api/analysis/drafts/api/analysis/drafts/{sid}/resumesession_id)./api/analysis/drafts/{sid}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.
/api/analysis/session/{sid}/equityequity_abs) would repeat equity, so they arrive as null with equity_abs_omitted: true and the client reuses equity./api/analysis/session/{sid}/metricswindow_from/window_to (inclusive ISO days) + rebase scope them to the framed period./api/analysis/session/{sid}/rolling/api/analysis/session/{sid}/allocationbacktests 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./api/analysis/session/{sid}/fx-effectexposure 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./api/analysis/session/{sid}/monthlywindow_from/window_to/rebase as the metrics, so the grid and the ledger describe the same period./api/analysis/session/{sid}/daily{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./api/analysis/session/{sid}/strategies/metricswindow_from/window_to/rebase: each leg is clipped and re-anchored with its own factor, returned alongside as window_factors./api/analysis/session/{sid}/weekday/{name}/statswindow_from/window_to clip them to the framed period (no rebase: one-contract dollars)./api/analysis/session/{sid}/weekday/{name}/series/api/analysis/session/{sid}/montecarloblock_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)./api/analysis/session/{sid}/equity-controljob_id./api/analysis/session/{sid}/equity-control/{job_id}running / done / error)./api/analysis/session/{sid}/walkforwardjob_id. Legacy module: retired from the UI, still served for direct callers./api/analysis/session/{sid}/walkforward/{job_id}running / done / error). Legacy./api/analysis/session/{sid}/correlation/api/analysis/session/{sid}/compare/correlationsubjects (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./api/analysis/variantsbase_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./api/reconcile/uploadoo_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./api/analysis/fxrefresh=true forces a re-read./api/analysis/session/{sid}/timeline/api/analysis/session/{sid}/timeline/window{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./api/analysis/session/{sid}/timeline/undo/redo) — rewrites the live session and returns it with the new history state./api/analysis/session/{sid}/timeline/recallseq or node_id, abandoned branches included.History is in-memory and volatile by design — see History.
/api/analysis/portfolio/saved/api/analysis/strategies/saved/api/analysis/variants/api/analysis/variants/{vid}/scratch/session/{sid}/… routes (also DELETE to close it)./api/analysis/variants/{vid}/commit/api/analysis/variants/{vid}/windown_trades: the trades of its visible strategies on that period./api/analysis/proporzionatore/runjob_id./api/analysis/proporzionatore/montecarlojob_id./api/analysis/proporzionatore/jobs/{job_id}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.
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.
/api/research/universe/api/research/data/status/api/research/days/api/research/events/catalogcoverage) and the next scheduled date; plus the calendar version and its last automatic update. Without a calendar on the server: available: false./api/research/events/preview{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./api/research/events/breakdown{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)./api/research/events?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./api/research/expirations?date=)./api/research/spot[minute of day, price] pairs./api/research/chaindate + 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./api/research/gex?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)./api/research/tape/api/research/templates/api/research/backtestjob_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./api/research/backtest/{job_id}running / done / error) and get its result. 404 if the job is unknown (for example after a restart)./api/research/saved{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./api/research/saved/refresh{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./api/research/saved/refresh/{job_id}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./api/research/saved/{filename}/api/research/saved/{filename}{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./api/research/saved/{filename}/to-strategy/api/research/saved/as-strategies/api/research/saved/{filename}/hide/api/research/strategy-recipe/{filename}/api/analysis/backtests/{filename}/to-session/api/research/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)./api/research/replay/what-ifstrikes 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./api/research/replay/portfolio-day{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./api/research/strike-guard/{job_id}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)./api/analysis/ai/compose-backtestRetry-After when a quota is used up, 503 when Veeman AI is at capacity for the day, 413 for a body over 4 MB./api/research/rebuildfile (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./api/research/rebuild/{job_id}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./api/analysis/ai/explain-rebuild{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./api/research/optimize/fields/api/research/studies/estimate/api/research/studies/api/research/studies/{sid}/api/research/studies/{sid}/cancel/api/research/studies/{sid}/resume/api/research/studies/{sid}/lockbox/api/research/studies/{sid}/recipe/api/research/models/api/research/models/{filename}/api/research/replay?backtest=&trade=)./api/research/replay{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./api/research/journal/api/research/journal/{filename}/api/research/drawings/{underlying}/{date}{list, rev}; an empty list and rev 0 for a day without drawings). They're personal, also inside a shared space./api/research/drawings/{underlying}/{date}{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)./api/research/tacticsbacktest_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./api/research/tactics/{filename}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.
/api/account/storageused_bytes, limit_bytes (null = no limit)./api/account/export/api/accountLimits 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.
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.
/api/spaces/api/spaces/{id}/api/spaces/{id}/invites/{token} to revoke, POST /{token}/accept to join)./api/spaces/{id}/members/{uid}/api/spaces/{id}/transfer/api/spaces/{id}/copy-incopy-out for the reverse)./api/spaces/{id}/activity/api/spaces/{id}/comments/{kind}/{name}/comment-counts returns them all at once./api/spaces/{id}/lease/{kind}/{name}/api/versions/{kind}/{name}/api/versions/{kind}/{name}/{rev}/api/versions/{kind}/{name}/{rev}/restore/api/versionsIf-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.