API Access · Data Products
Construct Data API
Subscribe to a Construct Data product, generate an API key, and pull daily-refresh factor data as signed-URL parquet. REST, Bearer-authed, no SDK required.
Getting started
- 1
Subscribe to a product at /lab/data — each subscription entitles your key to that product's data.
- 2
Generate an API key on your account page. The full key is shown once — store it securely.
- 3
Make your first request — the manifest endpoint is the cheapest check that your key + subscription work:
curl -H "Authorization: Bearer $NUMINOR_KEY" \
https://api.numinor.io/v1/constructs/sam-amplifier/manifestAuthentication
Send your key as a Bearer token on every request. One key works across all of your subscribed products (entitlements resolve from your active subscriptions). Matrix-tier keys do not grant raw REST export — that's an API-tier feature.
Authorization: Bearer nm_live_…Core concepts
- Point-in-time delay
- The API tier serves current data; the in-Matrix tier is delayed 30 days. Every row is explicit about its point-in-time so backtests are leak-free.
- Signed URLs
- Data endpoints return a short-lived signed S3 URL (≈4h). Download the parquet directly from S3 — that transfer is not metered or rate-limited.
- Parquet format
- All files are Apache Parquet. Read them with pyarrow, DuckDB, or polars — no server-side query endpoint needed for the common case.
- Rate limit
- Up to 1000 requests per minute per key. Over the limit returns 429 with a Retry-After header.
Endpoint reference
Base URL: https://api.numinor.io/v1
/constructs/{sku}/manifestData freshness, coverage window, and signed-URL TTL for a product.
| Parameter | In | Description |
|---|---|---|
| sku* | path | Product slug, e.g. sam-amplifier. |
curl -H "Authorization: Bearer $NUMINOR_KEY" \
https://api.numinor.io/v1/constructs/sam-amplifier/manifestResponse example
{
"sku": "sam-amplifier-construct-v1",
"tier": "api",
"status": "green",
"latest_trade_date": "2026-06-18",
"signed_url_ttl_seconds": 14400,
"historical_coverage": {
"start": "2016-01-04",
"end": "2026-06-18"
}
}/constructs/{sku}/day/{date}A signed URL to one trading day's partition.
| Parameter | In | Description |
|---|---|---|
| sku* | path | Product slug, e.g. sam-amplifier. |
| date* | path | Trading day as YYYYMMDD or YYYY-MM-DD. |
# get a signed URL for one trading day, then download the parquet
curl -H "Authorization: Bearer $NUMINOR_KEY" \
https://api.numinor.io/v1/constructs/sam-amplifier/day/20260515Response example
{
"url": "https://numinor-construct-data.s3.ap-northeast-2.amazonaws.com/…&X-Amz-Signature=…",
"trade_date": "2026-05-15",
"expires_in": 14400,
"format": "parquet"
}/constructs/{sku}/rangeSigned URLs for every published trading day in an inclusive date range.
| Parameter | In | Description |
|---|---|---|
| sku* | path | Product slug, e.g. sam-amplifier. |
| start* | query | Range start (inclusive), YYYYMMDD. |
| end* | query | Range end (inclusive), YYYYMMDD. |
curl -H "Authorization: Bearer $NUMINOR_KEY" \
"https://api.numinor.io/v1/constructs/sam-amplifier/range?start=20260501&end=20260531"Response example
{
"sku": "sam-amplifier-construct-v1",
"tier": "api",
"count": 21,
"days": [
{
"trade_date": "2026-05-06",
"url": "https://…signed…"
}
],
"format": "parquet"
}/constructs/{sku}/historicalA signed URL to the full historical bulk file (2016 → latest).
| Parameter | In | Description |
|---|---|---|
| sku* | path | Product slug, e.g. sam-amplifier. |
curl -H "Authorization: Bearer $NUMINOR_KEY" \
https://api.numinor.io/v1/constructs/sam-amplifier/historicalResponse example
{
"url": "https://…signed-bulk-file…",
"expires_in": 14400,
"format": "parquet"
}/constructs/{sku}/queryInline filtered query — on the roadmap (returns 501 today).
| Parameter | In | Description |
|---|---|---|
| sku* | path | Product slug, e.g. sam-amplifier. |
# roadmap — returns 501 today
curl -X POST -H "Authorization: Bearer $NUMINOR_KEY" \
https://api.numinor.io/v1/constructs/sam-amplifier/queryResponse example
{
"ok": false,
"error": "not_implemented",
"detail": "Roadmap — fetch signed URLs via /day or /range and read the parquet locally."
}Field-level export
Buy individual source fields — assembled as a cross-dataset recipe in the Matrix — and export them here. There's no product slug: one call exports every field your key's owner subscribes to. Export is async — POST returns a job id, poll it until the manifest is ready, then download one projected parquet per table from its signed URL. Each export re-materializes current data, so the same call always delivers fresh rows; URLs expire in ~3 days and your recipe is held indefinitely.
Base URL: https://api.numinor.io/v1
/field-export/statusCheap freshness check for a scheduled/conditional pull: returns your recipe's current vintage (no data, no materialization). Compare it — or use the ETag / If-None-Match header (304 when unchanged) — and only run /field-export when it changed.
# scheduled / conditional pull — only export when the data actually changed
LAST=$(cat .numinor_vintage 2>/dev/null)
NOW=$(curl -s -H "Authorization: Bearer $NUMINOR_KEY" https://api.numinor.io/v1/field-export/status | jq -r .vintage)
[ "$NOW" = "$LAST" ] && { echo "no new data"; exit 0; }
echo "$NOW" > .numinor_vintage
# → changed: run the export below, then load the fresh filesResponse example
{
"ok": true,
"vintage": "a1b2c3d4e5f6",
"datasets": [
{
"dataset": "rfp_bids",
"vintage": "9a2f1c4e7b83",
"latest_known_date": "20260728",
"major": 3
}
],
"note": "Compare `vintage` (or use the ETag) to your last pull; export only when it changed."
}/field-exportKick an async export of your entire active field recipe. Returns a job id immediately (202).
# 1) kick the export → job id
JOB=$(curl -s -X POST -H "Authorization: Bearer $NUMINOR_KEY" \
https://api.numinor.io/v1/field-export | jq -r .job_id)
# 2) poll until ready
until [ "$(curl -s -H "Authorization: Bearer $NUMINOR_KEY" \
https://api.numinor.io/v1/field-export/jobs/$JOB | jq -r .status)" = "ready" ]; do sleep 3; done
# 3) grab the signed per-table URLs
curl -s -H "Authorization: Bearer $NUMINOR_KEY" \
https://api.numinor.io/v1/field-export/jobs/$JOB | jq -r '.manifest.tables[].file.url'Response example
{
"ok": true,
"job_id": "b3f1c0a2-5e6d-4c7b-9a01-2f3e4d5c6b7a",
"status": "pending",
"reused": false,
"poll_url": "/v1/field-export/jobs/b3f1c0a2-5e6d-4c7b-9a01-2f3e4d5c6b7a"
}/field-export/jobs/{job_id}Poll a job. When status is ready it carries the signed, per-table download manifest; failed carries a reason.
| Parameter | In | Description |
|---|---|---|
| job_id* | path | The job id returned by POST /field-export. |
curl -H "Authorization: Bearer $NUMINOR_KEY" \
https://api.numinor.io/v1/field-export/jobs/b3f1c0a2-5e6d-4c7b-9a01-2f3e4d5c6b7aResponse example
{
"ok": true,
"status": "ready",
"manifest": {
"recipe": {
"datasets": [
"sam"
],
"table_count": 1,
"paid_field_count": 2
},
"url_expires_in_seconds": 259200,
"tables": [
{
"dataset": "sam",
"table": "fin_secu_sam_product",
"rows": 991882,
"columns": [
"secu",
"report_date",
"product_income",
"_known_date",
"_panel_op"
],
"keys": [
"secu",
"report_date"
],
"file": {
"name": "sam__fin_secu_sam_product.parquet",
"url": "https://…signed…"
}
}
],
"readme": "Each file is one table, projected to your fields + free join keys + version columns…"
}
}Errors
| Status | Error | Meaning |
|---|---|---|
| 401 | missing_bearer_token · invalid_api_key | Missing or invalid API key. |
| 403 | not_subscribed · rest_requires_api_tier | Your key has no API-tier subscription for this product (or only Matrix tier). |
| 404 | unknown_construct · no_partition | Unknown product, or no published partition for that date. |
| 400 | bad_date · bad_range · missing_params | Malformed date or range, or missing query parameters. |
| 413 | range_too_large | Range spans too many days — use the historical bulk file instead. |
| 429 | rate_limited | Rate limit exceeded (1000/min). Honor the Retry-After header. |
| 501 | not_implemented | Endpoint is on the roadmap and not implemented yet. |
Products
Ready to build? View pricing, then generate a key.