← DashboardMARKETSTAGE

Data API

MarketStage publishes the measurements behind the dashboard as plain JSON documents, so your own software can read them without a browser and without scraping a page. Derived output only — stages, scores, relative strength, momentum and percentage moves.

Closed by default, and access is granted by hand. Every path under /api/ sits behind a gate that answers before the request reaches our server, so an unauthenticated call is refused rather than served. There is no sign-up page and no button that mints a credential: you write to us and we issue one. Start at Getting access.

What it serves

The same numbers the dashboard shows, from the same run, computed once and written out as static files. There is no query language, no server-side computation and no per-request database: the pipeline writes the documents when it publishes, and the API hands them over. That is why it is fast, and why it can only answer with what the last run produced.

No prices, by design. Absolute closes, opens, highs, lows, volume and the sparkline series are deliberately absent, and a test in the build fails if one ever appears. Every level is expressed as a derived figure or a percentage. This is a licensing boundary, not an oversight — publishing price values is redistribution, which is not what our data agreements allow us to do.

End of day. Nothing here is intraday or a real-time quote feed, whatever cadence the dashboard itself runs at. The data_through field in every document tells you the last session it covers, and it is the field to trust.

Base URL and versioning

Everything lives under https://marketstage.eu/api/v1/. The version is part of the path, so a breaking change arrives as /api/v2/ and never as a surprise in a response you already parse. Only v1 exists today; a request to any other version is refused with an error that says so.

Only GET and HEAD are accepted (plus OPTIONS for CORS preflight), and only paths ending in .json resolve to anything.

Endpoints

EndpointWhat it returns
GET /api/v1/meta.jsonVersions, how many instruments are covered, when the data runs through, the horizon and update cadence, the producer, the MAR disclosure and the source attribution. Read this first; it is small and it dates everything else.
GET /api/v1/universe.jsonEvery instrument covered — ticker, name, kind, the page and group it belongs to, and the doc path of its own document. No measurements. This is where you look a document up before fetching it.
GET /api/v1/signals.jsonEvery instrument with its measurements, in one document. About 1 MB. One request for the whole board is cheaper for both of us than a thousand small ones.
GET /api/v1/signals/<doc>.jsonOne instrument. Take <doc> from the doc field in universe.json — do not build it from the ticker.
# open a session against the base URL
BASE=https://marketstage.eu/api/v1

# what is covered, and the disclosure that comes with it
curl -s "$BASE/meta.json"

# what is covered: tickers, names, kinds and doc paths, without any measurement
curl -s "$BASE/universe.json"

# every instrument with its measurements, in one document (about 1 MB)
curl -s "$BASE/signals.json"

# one instrument
curl -s "$BASE/signals/MSFT.json"

Never build a document path from a ticker

Most tickers are their own filename, so signals/MSFT.json works and tempts you into a rule. It is not a rule. Tickers that cannot be a filename as they stand — ^GSPC, GC=F — are rewritten with a short hash suffix that you cannot derive from anything you know. Every entry in universe.json, and every instrument document, carries a doc field with the exact path. Read it.

# right: ask universe.json where the document lives
curl -s "$BASE/universe.json" \
  | jq -r '.instruments[] | select(.ticker=="^GSPC") | .doc'
# -> signals/_GSPC-<hash>.json

# wrong: guessing the filename from the ticker
curl -s "$BASE/signals/^GSPC.json"     # 404, and always will be

Authentication

Two gates exist for this API and they present different credentials, so the first question to ask support is which one you have been issued. What follows describes both honestly, because a page that documents only the one we would like to be running would be no better than no page at all.

Cloudflare Access service token — what is in front of the API today

Access checks a service token before the request reaches us, and it does so without ever redirecting to a login page, which is exactly why a script can use it. The credential is a pair, and both halves go in headers on every request:

export CF_ACCESS_CLIENT_ID=...        # from the credential you were sent
export CF_ACCESS_CLIENT_SECRET=...    # keep both out of your repository

curl -s -H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
        -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
        "$BASE/signals/MSFT.json"

Under this gate every path is closed, meta.json and universe.json included, and a request without the pair is refused by Cloudflare with a 403 that is its own page, not the JSON error body described below. No per-key quota is counted. The secret is shown to us once when it is created and to you once when we send it; it belongs in the environment of whatever calls the API and never in a repository, a browser or a shared folder.

An API key — the key-and-quota gate, not yet in service

A key-based gate is written and reviewed but not deployed, and it is what a paid key will use when it is. It is documented here because its contract — the public endpoints, the error bodies and the quota headers in the rest of this page — is what you will be coding against, not the one you meet today. Ask us which gate is answering before you rely on either.

The key goes in one of two headers, whichever suits your client:

export MS_API_KEY=...   # the key we sent you; never paste it into a command line

# either header works; send one, not both
curl -s -H "Authorization: Bearer $MS_API_KEY" "$BASE/signals.json"
curl -s -H "X-API-Key: $MS_API_KEY"            "$BASE/signals.json"

Under this gate meta.json and universe.json are open without a key — you must be able to see what is covered, and what the disclosure says, before deciding whether to pay for the measurements. Everything else needs one. Keys are stored only as a hash, so nobody, us included, can read a working credential out of the store; if you lose one it is replaced, not recovered.

Errors

Under the key gate every refusal is a JSON document with the same two members, so one parser handles all of them. The HTTP status carries the category and error.code carries the reason; error.message is written for a human reading a log and may be reworded, so branch on the code and never on the text.

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Cache-Control: no-store

{
 "error": {
  "code": "missing_key",
  "message": "Send your key as 'Authorization: Bearer <key>' or 'X-API-Key: <key>'."
 },
 "api_version": 1
}
StatuscodeWhat happened
401missing_keyNo key in either header.
403invalid_keyThe key is not recognised. A revoked key looks the same as one that never existed.
403key_disabledThe key exists but has been switched off.
404unknown_versionThe path is under /api/ but not under /api/v1/. Only v1 exists.
404not_foundNo document at that path, or the path is not a .json document.
405method_not_allowedOnly GET, HEAD and OPTIONS are accepted.
429quota_exceededThe daily quota for your key is spent. See Quotas.
500misconfiguredOur fault, not yours. Tell us.
502origin_errorThe gate is up but the data origin is not answering. Retry with a backoff.

Error responses are sent with Cache-Control: no-store, so nothing between us caches a refusal and keeps serving it after the cause is fixed.

Quotas and what to do with a 429

A key carries a daily allowance, counted per key per day and reset at 00:00 UTC. Every authenticated response carries where you stand:

X-RateLimit-Limit: 5000       # your daily allowance
X-RateLimit-Remaining: 4873   # what is left, best-effort (see below)
X-RateLimit-Reset: 34190      # SECONDS until 00:00 UTC, not a timestamp
X-Plan: pro                   # the plan recorded against your key

X-RateLimit-Reset is a number of seconds until the reset, not a clock time and not a Unix timestamp. When the allowance is spent the answer is 429 with code: "quota_exceeded", the same quota headers, and a Retry-After in seconds. The correct response is to stop until it elapses. Retrying immediately spends nothing and achieves nothing — the counter only moves at midnight UTC.

The counter is best-effort, and we would rather say so than pretend. Two requests arriving at the same moment can read the same value, so a key may occasionally serve a handful of requests over its limit, and X-RateLimit-Remaining can be a little optimistic. It is a good guide and a bad invariant: treat the 429 as the authority. In the other direction, if the counter cannot be read at all the request is allowed through rather than refused — we would rather miscount than lock a paying caller out of their own data.

The cheapest way to stay well inside any quota is to stop asking for things you already have. One signals.json is one request for the entire board; a thousand per-instrument documents are a thousand. And nothing changes between publications, so polling faster than we publish returns the same bytes at the cost of your allowance.

Caching

Successful responses carry Cache-Control: public, max-age=300 and are cached at the edge for the same five minutes. The documents themselves change only when the pipeline republishes — once a day after the US close for the free set, and every 30 minutes between 08:00 and 22:00 Lisbon time on weekdays for Pro. Compare generated and data_through in meta.json against what you already hold and skip the download when they match; that is the whole of a sensible polling strategy. The status page shows whether the last run was healthy, which usually answers "why has this not moved" before you have to ask us.

CORS

The key gate answers preflight requests and sends Access-Control-Allow-Origin: * with GET, OPTIONS and the Authorization, X-API-Key and Content-Type headers allowed; preflight results may be cached for a day. Responses also carry X-Content-Type-Options: nosniff.

That the headers permit a browser call does not make one a good idea. Any credential you put in front-end JavaScript is a credential you have published, and it is your quota and your account that pay for it. Call the API from your server.

Document shapes

meta.json

Small, and the only document that dates the rest. disclosure_complete is worth checking in code: when it is false the producer identity behind the numbers is incomplete and disclosure_missing names what is absent.

{
 "api_version": 1,
 "app_version": "1.2.0",
 "generated": "2026-08-18T14:36:10",
 "data_through": "2026-08-18",
 "instruments": 1028,
 "kinds": ["commodity", "equity", "etf", "eu_sector", "fund",
           "industry", "macro", "sector", "stock"],
 "horizon": "26 weeks (medium term). ...",
 "update_frequency": "Recalculated on every run, on end-of-day data. ...",
 "producer": "...",
 "disclosure_complete": true,
 "disclosure_missing": [],
 "disclaimer": "These are investment recommendations within the meaning of ...",
 "attribution": ["Euro foreign exchange reference rates: European Central Bank ...", "..."],
 "notes": ["Derived signals only. ...", "End-of-day. ..."]
}

universe.json

{"api_version", "data_through", "instruments"}, where each instrument carries id, doc, ticker, name, kind, page and group — and nothing else. It is coverage, not measurement, which is what makes it safe to leave open.

signals/<doc>.json and signals.json

One instrument under an instrument key, or every instrument in an instruments array. The instrument object is identical in both, so the same parser reads either.

{
 "api_version": 1,
 "data_through": "2026-08-18",
 "instrument": {
  "id": "stk:IGV:MSFT",
  "doc": "signals/MSFT.json",
  "ticker": "MSFT",
  "name": "Microsoft",
  "kind": "stock",
  "page": "stocks",
  "group": null,
  "industry": "Software",
  "stage": {
   "d": {"n": 2, "sub": "A", "since": "2026-08-06", "bars": 8},
   "w": {"n": 4, "sub": "B", "since": "2026-02-06", "bars": 29},
   "m": {"n": 2, "sub": "B", "since": "2023-04-30", "bars": 41}
  },
  "score": 31,
  "rs_rating": 37,
  "mansfield": -3.411132444244902,
  "mansfield_rising": true,
  "minervini": 5,
  "faber": true,
  "momentum_12_1": -22.42218995598129,
  "short_term": "wait",
  "golden_cross": false,
  "leader": false,
  "tags": [],
  "pos_52w_pct": 67.49503098011259,
  "chg_1d_pct": -3.4608375504450173,
  "chg_1m_pct": 21.439996949947048,
  "chg_1y_pct": -7.733279701552331,
  "stage_ribbon": "2222222222222222222222222222334411444444...",
  "asof": "2026-08-18",
  "stale": false
 }
}

Instrument fields

id
Our internal identity for the row. Opaque, and scoped to the page that ranked it — match on ticker, never on this.
doc
Where this instrument's own document lives, relative to /api/v1/.
ticker, name, kind
The identity you should key on. kind is one of the values listed in meta.json → kinds.
page, group, industry
Where the instrument sits on the dashboard. Any of them may be null; they are presentation, not classification you should depend on.
stage
The classification, per timeframe: d daily, w weekly (the primary read), m monthly. Each is {"n", "sub", "since", "bars"} or null when there is not enough history. n is 1 accumulation, 2 advance, 3 distribution, 4 decline — see how to read the stages. sub is "A" for a young stage and "B" for a mature one, since is the date it was entered and bars how many bars of that timeframe it has lasted.
score
The weighted composite, 0–100, higher being stronger. Comparable between instruments on the same run; not comparable across runs as an absolute level.
rs_rating
Percentile relative strength, 1–99, against the rest of the stock universe. null for anything that is not a universe stock with a year of history.
mansfield, mansfield_rising
Mansfield relative strength against the benchmark, and whether it is turning up.
minervini
How many of the eight trend-template conditions are met, 0–8.
faber, golden_cross, leader
Booleans: above the 10-month average, 50-day above 200-day, and whether the instrument is flagged as a leader in its group.
momentum_12_1
Twelve-month momentum excluding the most recent month, as a percentage.
short_term
"go", "wait" or "no" — a state, not an instruction. "wait" means the trend is not against you but there is no clean entry.
pos_52w_pct, chg_1d_pct, chg_1m_pct, chg_1y_pct
Position within the 52-week range and the moves over a day, a month and a year, all as percentages. Percentages, never levels — see no prices.
stage_ribbon
Up to 260 characters, one per weekly bar, oldest first: the digit is the stage that week and "." is a week with no classification.
tags
Free-form labels attached by the model. Informational.
asof, stale
The date this instrument's own data runs through, and whether that is behind the rest of the run. A true here is the single most useful field on the document: it says this row is older than it looks.

A field may be null whenever the model could not compute it — too little history, no benchmark, an instrument the measure does not apply to. Treat null as "not known", never as zero, and expect new fields to appear within v1: additions are not breaking changes, so parse leniently and ignore what you do not recognise.

Getting access

There is no self-service page, and that is deliberate rather than unfinished: credentials are issued one at a time, by hand, against a name we can contact. Write to [email protected] from the address on your subscription and tell us what you intend to build, roughly how many requests a day it needs, and whether it runs on a server or on someone's machine. We answer within 2 business days.

You will get back the credential itself, which is shown once and is not recoverable afterwards, and the allowance recorded against it. To rotate one, ask for a replacement and switch over before we retire the old one; to revoke one, tell us and it stops working immediately. If you think a credential has leaked, say so first and apologise later — disabling one takes a minute.

API access is part of the Pro plan; see pricing for what that includes. We are not publishing a separate API price list, because there is not an honest one to publish yet.

Terms of use

This API is the supported way to read MarketStage with software. Scraping the HTML instead is a breach of our Terms and Conditions, which also cover what you may do with what you read: use it in your own tools freely, but do not resell or redistribute the output, and do not build a competing product out of it. Credentials are per customer, not per team-that-shares-one.

Attribution travels with the data. meta.json carries an attribution array identifying the official institutions and the applicable market-data source class behind the underlying series; if you show these numbers to anyone else, carry it with them.

Educational tool — not investment advice. What this API serves is general information produced by an automated model on end-of-day data. They take no account of your circumstances, they are not a personal recommendation, and we are not supervised by the CMVM. meta.json carries the full disclaimer in the response itself, which is where it belongs.

MarketStage

Help

Company

Legal