# Analyze & Recommend Source: https://eomer.mintlify.app/api-reference/agent-analyze POST /agent/analyze Profile a CSV and return the recommended forecast configuration. No job is created — this is a synchronous helper so the frontend can render an explanation panel before the user runs anything. # Extract Forecast Intent Source: https://eomer.mintlify.app/api-reference/agent-extract POST /agent/extract Turn a natural-language prompt + dataset into a typed ForecastIntent. Synchronous, no job is created. The intent's ``clarification_questions`` drive the chat follow-up loop; the frontend fills answers and re-validates via ``POST /agent/validate`` (no model call) before confirming a run. # Agent Forecast Source: https://eomer.mintlify.app/api-reference/agent-forecast POST /agent/forecast Profile a CSV, pick a preset automatically, and submit the forecast. # Validate Forecast Intent Source: https://eomer.mintlify.app/api-reference/agent-validate POST /agent/validate Re-validate a (partially edited) intent against the dataset columns. The deterministic clarify-loop step: the frontend merges the user's answers into ``intent`` and posts it here to recompute ``missing_required_fields`` / ``clarification_questions`` / ``review_status`` with no model call. # Errors Source: https://eomer.mintlify.app/api-reference/errors Every error response carries the same envelope: a human-readable detail plus a machine-readable code, a retry hint and the request id. All non-2xx responses share one JSON envelope: ```json theme={null} { "detail": "Unsupported file type '.txt'. Allowed: ['.csv', '.parquet', '.xls', '.xlsx']", "error_code": "validation_error", "retryable": false, "request_id": "6f1c2a9e-…" } ``` | Field | Type | Meaning | | ------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `detail` | string or object | Human-readable explanation. For structured errors it is an object with its own `error_code`, `detail`, optional `suggestion`, `retryable` and `retry_after_seconds`. Its shape is stable — clients that only read `detail` keep working. | | `error_code` | string | Machine-readable code (below). Mirrors `detail.error_code` when present. | | `retryable` | boolean | `true` when the same request may succeed later (rate limits, temporary overload, timeouts). | | `request_id` | string | Echoes the `X-Request-ID` response header. Quote it when contacting support. | Validation failures on request parameters (HTTP 422) put the standard list of field errors in `detail` and use `error_code: validation_error`. ## Error codes | `error_code` | HTTP | When | | ----------------------------- | ------------------ | --------------------------------------------------------------------------------------- | | `validation_error` | 400, 413, 415, 422 | Bad input: file type, schema, form values, JSON options, or a request over an input cap | | `dataset_too_large` | 413 | Row or column count over the deployment's limit — `detail.suggestion` says what to do | | `file_too_large` | 413 | Upload over the byte limit | | `invalid_preset` | 400 | Unknown model tier | | `auth_failed` | 401, 403 | Missing, invalid or insufficient API key | | `job_not_found` | 404 | No such job for this key | | `job_not_completed` | 409 | Download requested before the job finished | | `rate_limited` | 429 | Too many requests — honour `Retry-After` | | `server_overloaded` | 503 | Job queue full — retry with backoff | | `compute_unavailable` | 503 | No compute worker reachable — retry later | | `tabular_runtime_unavailable` | 503 | Classification/regression is not enabled on this deployment | | `fine_tune_disabled` | 503 | Fine-tuning is not enabled on this deployment | | `timeout` | 504 | The job exceeded its time budget | | `internal_error` | 500 | Unexpected failure; the `request_id` identifies it in our logs | Failed jobs carry the same code on `GET /jobs/{job_id}` in the `error_code` field next to the human-readable `error`. # Submit Forecast Source: https://eomer.mintlify.app/api-reference/forecast POST /forecast # Forecast output Source: https://eomer.mintlify.app/api-reference/forecast-output The columns, timestamp format and download formats of a completed forecast job. `GET /jobs/{job_id}/download` returns the forecast as a table with one row per series and forecast step. ## Columns | Column | Type | Meaning | | ---------------------- | --------- | ------------------------------------------------------------------------------- | | `item_id` | string | The series identifier from your input | | `timestamp` | timestamp | The forecasted step | | `mean` | float | Point forecast | | `0.1`, `0.5`, `0.9`, … | float | One column per requested quantile level (`quantile_levels`), named by the level | Columns appear in that order. If you did not pass `quantile_levels`, the model's default levels are included. ## Rendering options Two optional form fields on `POST /forecast` (and JSON fields on the storage-object and batch variants) change how the table is rendered. Defaults preserve the output exactly as it has always been produced. | Field | Values | Default | Effect | | ----------------------- | ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- | | `timestamp_format` | `pandas`, `iso8601` | `pandas` | `pandas` renders `2026-01-01 00:00:00`; `iso8601` renders `2026-01-01T00:00:00+00:00` (timestamps are UTC) | | `quantile_column_style` | `raw`, `prefixed` | `raw` | `raw` names quantile columns `0.1`; `prefixed` names them `q0.1` (friendlier for tools that dislike numeric headers) | ## Download formats `GET /jobs/{job_id}/download?format=…` | `format` | Response | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | omitted | The default: a `307` redirect to a short-lived signed URL when the result is one published file, otherwise the CSV itself, or a zip when the job produced several files | | `csv` | The CSV streamed through the API (`text/csv`) | | `parquet` | The same table as Parquet (`application/octet-stream`) | | `zip` | Every output file bundled as `results_{job_id}.zip` | | `presign` | Always a `307` redirect to a signed URL (single-file results only) | An unsupported combination — for example `presign` on a multi-file result — is a `400` with `error_code: validation_error` and a `detail` naming the format to use instead. # Submit Forecast From Storage Object Source: https://eomer.mintlify.app/api-reference/forecast-storage-object POST /forecast/storage-object # Health Check Source: https://eomer.mintlify.app/api-reference/health GET /health Public health/readiness check; verifies upload/output dirs are writable. Returns HTTP 503 when any CRITICAL local check fails (unwritable dirs) so orchestrator readiness probes pull a degraded instance out of rotation. For pure liveness (process/event-loop alive) use ``GET /livez``. # Job Status & Management Source: https://eomer.mintlify.app/api-reference/jobs GET /jobs/{job_id} # List Models Source: https://eomer.mintlify.app/api-reference/models GET /models # Complete Upload Source: https://eomer.mintlify.app/api-reference/storage-complete-upload POST /storage/uploads/{object_id}/complete # List Storage Objects Source: https://eomer.mintlify.app/api-reference/storage-list-objects GET /storage/objects # Presign Download URL Source: https://eomer.mintlify.app/api-reference/storage-presign-download GET /storage/objects/{object_id}/download-url # Presign Upload URL Source: https://eomer.mintlify.app/api-reference/storage-presign-upload POST /storage/uploads/presign # Preview Storage Object Source: https://eomer.mintlify.app/api-reference/storage-preview-object GET /storage/objects/{object_id}/preview # Submit Classification v2 Source: https://eomer.mintlify.app/api-reference/v2-classify POST /v2/classify # Submit Classification Storage Object v2 Source: https://eomer.mintlify.app/api-reference/v2-classify-storage-object POST /v2/classify/storage-object # Submit Regression v2 Source: https://eomer.mintlify.app/api-reference/v2-regress POST /v2/regress # Submit Regression Storage Object v2 Source: https://eomer.mintlify.app/api-reference/v2-regress-storage-object POST /v2/regress/storage-object # Enterprise Data Connectors Source: https://eomer.mintlify.app/guides/connectors Connect your own object storage (S3, Cloudflare R2, MinIO) as a forecasting data source — with per-connection, encrypted, IAM-first credentials. The S3-compatible connector (AWS S3, Cloudflare R2, MinIO) is fully wired into the REST API: create a connection once, then test, validate, and forecast against it. Connection-management calls require a tenant identity (an API key with an organization, or an `EOMER_API_KEY_TENANT_MAP_JSON` mapping for local keys). ## What a connector gives you A **data connection** is a tenant-owned, reusable pointer to *your* data system, with credentials handled securely on eomer's side. Once configured, eomer can: * **Test** connectivity and authentication (`test_connection`) * **Discover** objects under a prefix (`list_objects`) and **estimate** volume (`estimate_size`) * **Infer schema** from a bounded sample (`infer_schema`) * **Validate** the data as a forecasting dataset (`validate_source`) * **Read** the data for a forecast job (`read_data`) * **Write** forecast results back to a destination (`write_output`) ## Credential model (IAM-first) | Auth method | Secret stored? | Use when | | --------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sts_assume_role` | **No** | **Recommended for AWS** — you grant eomer's AWS identity `sts:AssumeRole` on a read-only role (`role_arn` + `external_id` on the connection); eomer uses short-lived STS credentials. | | `iam_role`, `instance_profile`, `workload_identity` | **No** | Same-account / workload-identity setups. | | `access_key` | Yes — envelope-encrypted | Cloudflare R2, MinIO, or S3 without role access. | | `sas_token`, `service_account_json` | Yes — envelope-encrypted | Reserved for Azure / GCS in a later phase. | When a secret is stored, it is encrypted with a Fernet key from `EOMER_CONNECTOR_ENCRYPTION_KEY` **before persistence**. API responses never return secrets — only a redacted summary (`auth_method`, a masked hint, key version). See the package `README.md` for rotation and the KMS path. ## REST quickstart ```bash theme={null} BASE=https://api.eomer.ai # or http://localhost:8000 AUTH="Authorization: Bearer $EOMER_API_KEY" # 1. Create a connection (Cloudflare R2 — credentials are sealed at rest) curl -s -X POST "$BASE/data-connections" -H "$AUTH" -H "Content-Type: application/json" -d '{ "connector_type": "r2", "name": "sales-lake", "object_storage": { "provider": "cloudflare_r2", "bucket": "company-forecasting-data", "prefix": "sales/daily/", "endpoint_url": "https://.r2.cloudflarestorage.com", "region": "auto", "auth_method": "access_key", "file_format": "csv", "credentials": {"access_key_id": "", "secret_access_key": ""} } }' # → 201 {"connection_id": "conn_ab12cd34ef56", "status": "untested", # "credential": {"auth_method": "access_key", "hint": "***REDACTED...", ...}} CONN=conn_ab12cd34ef56 # 2. Test connectivity (status is persisted on the connection) curl -s -X POST "$BASE/data-connections/$CONN/test" -H "$AUTH" # 3. Validate as a forecasting dataset curl -s -X POST "$BASE/data-connections/$CONN/validate" -H "$AUTH" \ -H "Content-Type: application/json" \ -d '{"item_id_column": "store_id", "timestamp_column": "date", "target_column": "sales"}' # 4. Submit a forecast straight from the connection curl -s -X POST "$BASE/data-connections/$CONN/forecast" -H "$AUTH" \ -H "Content-Type: application/json" \ -d '{"prediction_length": 14, "presets": "small", "item_id_column": "store_id", "timestamp_column": "date", "target_column": "sales"}' # → 202 {"job_id": "...", "status": "pending", ...} — poll GET /jobs/{job_id} as usual # 5. Review the audit trail curl -s "$BASE/data-connections/audit?connection_id=$CONN" -H "$AUTH" ``` Responses never contain credentials — only a redacted `credential` summary (auth method, masked key hint, key version). ## Configure an S3-compatible source (Python library) ### AWS S3 (keyless) ```python theme={null} from eomer_forecasting.connectors import get_enterprise_connector from eomer_forecasting.connectors.types import ConnectorType, ConnectorProvider from eomer_forecasting.connectors.credentials import AuthMethod from eomer_forecasting.connectors.schemas import ( DataConnectionSpec, ObjectStorageConnectionSpec, ForecastDatasetSpec, ) spec = DataConnectionSpec( tenant_id="org_123", connector_type=ConnectorType.s3, name="sales-lake", object_storage=ObjectStorageConnectionSpec( provider=ConnectorProvider.aws_s3, bucket="company-forecasting-data", prefix="sales/daily/", region="us-east-1", auth_method=AuthMethod.iam_role, # no secret stored file_format="parquet", ), ) connector = get_enterprise_connector(ConnectorType.s3) print(connector.test_connection(spec)) ``` ### Cloudflare R2 R2 is S3-compatible, so it reuses the **same connector** — only the endpoint, region, and (sealed) access key differ: ```python theme={null} from eomer_forecasting.connectors.credentials import SecretCipher, AuthMethod cred = SecretCipher.from_env().seal( AuthMethod.access_key, {"aws_access_key_id": "", "aws_secret_access_key": ""}, hint_field="aws_access_key_id", ) os_spec = ObjectStorageConnectionSpec( provider=ConnectorProvider.cloudflare_r2, bucket="company-forecasting-data", prefix="sales/daily/", endpoint_url="https://.r2.cloudflarestorage.com", region="auto", auth_method=AuthMethod.access_key, credential=cred, file_format="parquet", ) ``` ## Validate before you forecast ```python theme={null} dataset = ForecastDatasetSpec( item_id_column="store_id", timestamp_column="date", target_column="sales", known_covariates_columns=["price", "promotion_flag"], ) report = connector.validate_source(spec, dataset) if not report.is_valid: print(report.errors, report.error_codes) print("frequency:", report.inferred_frequency, "rows:", report.row_count) ``` `validate_source` reuses eomer's core validator (required columns, null checks, numeric target, duplicate entity-time rows, parseable timestamps, minimum history) and adds frequency inference, schema-drift detection, and a volume guardrail. ## Deliver results back to your system Add an `output` block to the forecast request and eomer writes the completed forecast straight back to your bucket — closing the loop with no polling or manual download: ```bash theme={null} curl -s -X POST "$BASE/data-connections/$CONN/forecast" -H "$AUTH" \ -H "Content-Type: application/json" -d '{ "prediction_length": 14, "presets": "small", "item_id_column": "store_id", "timestamp_column": "date", "target_column": "sales", "output": { "key_template": "eomer-forecasts/{date}/{job_id}.csv", "file_format": "csv", "allow_overwrite": false } }' ``` * `output.connection_id` may point at a **different** connection (it must belong to your tenant); omitted = write back to the source connection. * `key_template` supports `{job_id}` and `{date}` placeholders; the default is `eomer-forecasts/{date}/{job_id}.csv`. Formats: csv, json, jsonl, parquet. * Delivery never overwrites existing objects unless `allow_overwrite: true`. * Track it on the job: `GET /jobs/{job_id}` returns `delivery_status` (`pending → delivering → delivered | failed | skipped`), `delivery_uri`, and a sanitized `delivery_error`. A failed delivery **never** fails the forecast — the result stays downloadable and the attempt is recorded in the audit trail (`action: "deliver"`). ## Partitioned data, globs, and compression * A trailing-slash `prefix` reads and concatenates **all objects** under it; add `key_pattern` (e.g. `"part-*.csv"`, `"2026-*/part-*.parquet"`) to filter. * Set `compression: "gzip"` on the connection for gzip'd CSV/JSON objects (`zstd` supported with the optional `zstandard` package); Parquet's internal codec needs no configuration. * Forecast submission returns **202 immediately** — the connector read happens inside the job worker, and a failed read marks the job `failed` with the sanitized connector error (visible on `GET /jobs/{job_id}` and in the audit trail). ## Safety guarantees * Reads are **byte- and row-bounded** (`max_bytes` / `max_rows`) — oversized sources raise `DATASET_TOO_LARGE` instead of OOMing. * Object keys are sanitised against path traversal and control characters. * Format **magic bytes** are checked (a Parquet file declared as CSV fails fast). * Writes **refuse to overwrite** existing objects unless explicitly allowed, and can apply server-side encryption. * Every failure maps to a stable `ConnectorErrorCode` (`AUTHENTICATION_FAILED`, `AUTHORIZATION_FAILED`, `SOURCE_NOT_FOUND`, `RATE_LIMITED`, …); provider error text is never echoed back. # Enterprise Data Handoff (Spark, Hadoop, Lakehouse) Source: https://eomer.mintlify.app/guides/enterprise-data-handoff Push Parquet snapshots to R2 from Spark/Hadoop, or have eomer pull directly from Azure/GCS/WebHDFS. Both paths use the same _SUCCESS marker contract. Enterprise data teams run their analytics on Spark, Hadoop, and lakehouse stacks (Iceberg, Delta). There are two ways to feed that data into eomer: * **Push to R2** (recommended) — the upstream job writes Parquet directly to your tenant's R2 bucket via the `s3a://` connector. * **Pull via `cloud_fs`** (fallback) — eomer reads directly from the customer's Azure/GCS/WebHDFS/S3-compatible store. For regulated or air-gapped environments where a cross-cloud copy is not an option. Both paths use the same Spark/Hadoop `_SUCCESS` marker contract, so the consumer never sees a half-written prefix. This guide covers the producer configuration for push, the eomer-side config for both directions, and how to decide which one fits. ## Why push to R2 instead of pulling * **No VPN peering, no firewall holes.** The customer's cluster writes outbound to an HTTPS endpoint. * **One auth surface.** The same R2 token the customer already has covers both direct uploads and Spark pushes. * **Works with any S3-compatible producer.** Spark, Flink, Trino, DuckDB, `hadoop distcp`, Databricks, EMR — all support the `s3a://` scheme out of the box. * **The eomer forecasting model uses batch input.** A coherent history snapshot per job run is the right semantic for forecasting; streaming event feeds would need to be materialized into the same shape regardless. ## Producer configuration ### Apache Spark 3.x ```python theme={null} spark = ( SparkSession.builder .appName("eomer-forecast-export") .config("spark.hadoop.fs.s3a.endpoint", "https://.r2.cloudflarestorage.com") .config("spark.hadoop.fs.s3a.access.key", "") .config("spark.hadoop.fs.s3a.secret.key", "") .config("spark.hadoop.fs.s3a.path.style.access", "true") .config("spark.hadoop.fs.s3a.endpoint.region", "auto") .getOrCreate() ) snapshot_prefix = f"s3a://eomer-production-tenant-acme/data/{run_date}/" (history_df .select("item_id", "timestamp", "target") .write .mode("overwrite") .parquet(snapshot_prefix)) ``` Spark's Parquet writer emits a `_SUCCESS` marker automatically when all part-files are durable. That's all eomer needs to know the snapshot is safe to read. ### Hadoop `distcp` ```bash theme={null} hadoop distcp \ -Dfs.s3a.endpoint=https://.r2.cloudflarestorage.com \ -Dfs.s3a.access.key= \ -Dfs.s3a.secret.key= \ -Dfs.s3a.path.style.access=true \ hdfs:///user/analytics/forecast_input/2026-04-17/ \ s3a://eomer-production-tenant-acme/data/2026-04-17/ ``` `distcp` writes `_SUCCESS` at the destination once all files copy cleanly. ### Other producers Any tool that speaks S3 works — Flink, Trino, DuckDB (`COPY ... TO 's3://...'`), pandas + `s3fs`. Make sure the job writes a `_SUCCESS` (or equivalent) marker *after* the data files are durable. If the tool doesn't emit one, add a final step: `aws s3 cp /dev/null s3://bucket/prefix/_SUCCESS --endpoint-url https://...`. ## Consumer contract (eomer side) ### Minimum: require `_SUCCESS` before reading ```yaml theme={null} ingestion: source: type: r2 r2_options: bucket: eomer-production-tenant-acme key: "data/2026-04-17/" file_format: parquet require_success_marker: true # default; fails fast if missing ``` If the marker is absent, the run aborts with a clear error — eomer never sees a half-written snapshot. `require_success_marker` is ignored when `key` points at a single object (no slash). ### Rolling pointer: `watermark_file` Producers that write dated snapshots and keep history should update a pointer file atomically once each snapshot completes: ``` r2://bucket/data/2026-04-16/ (old, still retained) r2://bucket/data/2026-04-17/ (new, with _SUCCESS) r2://bucket/latest.txt ← contents: "data/2026-04-17/" ``` Point eomer at the pointer: ```yaml theme={null} ingestion: source: type: r2 r2_options: bucket: eomer-production-tenant-acme key: "" # ignored when watermark_file is set file_format: parquet watermark_file: "latest.txt" require_success_marker: true ``` eomer reads `latest.txt`, trims the contents, and uses them as the effective prefix. This decouples the eomer config from the snapshot rotation: the customer's scheduler owns the pointer; eomer always reads whatever the pointer says "latest" is. A runnable example is in [`configs/example_r2_spark_push.yaml`](https://github.com/eomer-ai/eomer/blob/main/configs/example_r2_spark_push.yaml). ## Pull fallback: read directly from the customer's cloud Push-to-R2 is the recommended primary path: one auth surface, no VPN peering, producer-side handoff contract. When it's not viable — regulated environments that disallow cross-cloud copies, customers who already have fresh data in their own Azure/GCS/WebHDFS store, or short pilots where setting up a Spark job is overkill — eomer can pull directly via the `cloud_fs` connector. One connector covers every fsspec-supported backend: | Backend | URI scheme | Extras to install | | ------------------------------------------------------ | ------------------------------ | ------------------------------------------------------------------------ | | Azure Data Lake / Blob | `abfs://`, `abfss://`, `az://` | `pip install 'eomer-forecasting[azure]'` | | Google Cloud Storage | `gs://`, `gcs://` | `pip install 'eomer-forecasting[gcp]'` | | S3-compatible (MinIO, Wasabi, AWS S3 in a tenant acct) | `s3://`, `s3a://` | `pip install 'eomer-forecasting[s3]'` | | Hadoop WebHDFS | `webhdfs://host:port/path` | `pip install 'eomer-forecasting[s3]'` (fsspec ships the WebHDFS backend) | For **Cloudflare R2** keep using `type: r2` — the dedicated connector has stricter `EOMER_R2_*` credential handling and a simpler config. The `cloud_fs` validator will reject `r2://` URIs and point you back to the R2 connector. ### Azure Data Lake example ```yaml theme={null} ingestion: source: type: cloud_fs cloud_fs_options: uri: "abfs://forecast-inputs@tenantaccount.dfs.core.windows.net/data/2026-04-17/" file_format: parquet require_success_marker: true storage_options: account_name: "tenantaccount" # account_key / sas_token / client_id etc. resolved from env vars # — never embed secrets in the config itself. ``` ### GCS example ```yaml theme={null} ingestion: source: type: cloud_fs cloud_fs_options: uri: "gs://tenant-forecast-inputs/data/2026-04-17/" file_format: parquet require_success_marker: true # GCS uses Application Default Credentials by default # (GOOGLE_APPLICATION_CREDENTIALS pointing at a service-account JSON). ``` ### WebHDFS example ```yaml theme={null} ingestion: source: type: cloud_fs cloud_fs_options: uri: "webhdfs://hadoop-edge.internal:50070/user/analytics/forecast_input/2026-04-17/" file_format: parquet require_success_marker: true storage_options: user: "eomer-service-account" ``` The `_SUCCESS` marker contract is identical across all backends — the connector uses the same Spark/Hadoop convention as the R2 push path. Credentials are passed verbatim to fsspec under `storage_options`; never embed secrets in configs. A runnable example is in [`configs/example_cloud_fs.yaml`](https://github.com/eomer-ai/eomer/blob/main/configs/example_cloud_fs.yaml). ### Choosing between push and pull | | Push to R2 | Pull via `cloud_fs` | | ------------------- | --------------------------------- | -------------------------------------------------------------- | | Network direction | Customer → R2 (HTTPS) | eomer → customer's store (HTTPS) | | Works across clouds | Yes | Yes (but their firewall must allow eomer's egress IP) | | Handoff contract | `_SUCCESS` marker, producer-owned | `_SUCCESS` marker, producer-owned | | Credential scope | One R2 token, issued by us | Customer-issued read-only token/role | | Best for | Most enterprise customers | Air-gapped / regulated / "already in our cloud, don't move it" | ## Troubleshooting | Symptom | Likely cause | Fix | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `No '_SUCCESS' marker found at r2://.../data/2026-04-17/_SUCCESS` | Spark job still running, or failed partway through | Check the upstream job's state. If the marker is intentionally absent, set `require_success_marker: false`. | | `AccessDenied` from boto3 | R2 token missing `Object Read` on the bucket | Recreate the token at Cloudflare dashboard → R2 → Manage R2 API Tokens with both read and write. | | `SignatureDoesNotMatch` | Wrong region or endpoint | R2 must use `region: auto` and the endpoint `https://.r2.cloudflarestorage.com`. Spark needs `spark.hadoop.fs.s3a.path.style.access=true`. | | `Watermark file ... could not be read` | Pointer file missing or wrong bucket | Verify the object exists: `aws s3 ls s3://bucket/latest.txt --endpoint-url https://...`. | | `No objects found under r2://.../data/...` | Prefix empty or marker present but no part-files (Spark wrote empty partition) | Verify the upstream job produced data rows; check Spark's output plan. | | `cloud_fs uri '...' uses unsupported protocol 'r2'` | Tried to use `cloud_fs` for Cloudflare R2 | Switch to `type: r2` with `r2_options` — the dedicated connector is intended for R2. | | `The fsspec backend for 'abfs' is not installed` (or `gs`, etc.) | Missing optional extra | `pip install 'eomer-forecasting[azure]'` (Azure) or `[gcp]` (GCS). Both are included in `[all]`. | See also [Tenant-Isolated Storage (R2)](/guides/tenant-storage-r2) for bucket naming and credential setup. # Cost Estimation & Covariates Source: https://eomer.mintlify.app/guides/estimate-and-covariates Get transparent cost/runtime estimates and enrich forecasts with external data ## Cost Estimation Before submitting a forecast job, use `POST /estimate` to get a transparent cost and runtime estimate. ### Request ```bash theme={null} curl -X POST https://api.eomer.ai/estimate \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "n_series": 10, "n_timesteps": 365, "frequency": "D", "forecast_horizon": 30, "preset": "eomer_pulse", "requested_covariates": ["temperature_2m"], "latitude": 52.52, "longitude": 13.405 }' ``` ### Request Fields | Field | Type | Default | Description | | ---------------------- | ---------- | --------------- | ---------------------------------- | | `n_series` | int (>= 1) | *required* | Number of unique time series | | `n_timesteps` | int (>= 1) | *required* | Timesteps per series | | `frequency` | string | `"D"` | Time frequency: `D`, `W`, `M`, `H` | | `forecast_horizon` | int (>= 1) | `24` | Steps to forecast | | `preset` | string | `"eomer_pulse"` | Model preset | | `requested_covariates` | string\[] | `[]` | Covariate variables to include | | `latitude` | float | `null` | Required for weather covariates | | `longitude` | float | `null` | Required for weather covariates | ### Response ```json theme={null} { "estimated_runtime_seconds": 12.5, "estimated_compute_cost_usd": 0.0055, "maximum_cost_usd": 0.01375, "suggested_quote_usd": 0.00825, "backend": "compute worker", "breakdown": { "base_runtime_s": 15.0, "data_runtime_s": 8.76, "horizon_runtime_s": 1.8, "covariate_runtime_s": 0.45, "preset_multiplier": 3.0, "total_runtime_s": 12.5, "compute_cost_usd": 0.0055 }, "available_covariates": [ "cpi", "fed_funds_rate", "precipitation", "relative_humidity_2m", "temperature_2m", "treasury_10y", "wind_speed_10m" ] } ``` ### What Drives Cost | Factor | Impact | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Preset size** | `eomer_pulse_s` (1x) \< `eomer_pulse_l` (3x) \< `eomer_horizon_s` (5x) \< `eomer_horizon_m` (8x) | | **Data volume** | Linear scaling with `n_series * n_timesteps` | | **Forecast horizon** | Minor: \~0.02s per additional step | | **Covariates** | Minor: \~0.15s per covariate column | | **Backend** | A shared compute node is free; dedicated compute workers bill by runtime at the rate shown in `breakdown.compute_rate_usd_hr` | *** ## Covariates Enrich your forecasts with external data. The recommended interface is the structured **`external_covariates`** field — accepted as a JSON string form field on `POST /forecast`, and as a JSON object in the body of `POST /forecast/storage-object` and `POST /forecast/batch`. ### Discover available sources `GET /covariates/catalog` returns every source, its required location key, its variables, the generated output column names, and which cities/zones are pre-materialized: ```bash theme={null} curl https://api.eomer.ai/covariates/catalog -H "Authorization: Bearer $API_KEY" ``` ### Sources & variables | Source | Entity key | Variables | | ------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `weather` | `city` *or* `latitude_column` + `longitude_column` | `temperature_2m`, `precipitation`, `wind_speed_10m`, `relative_humidity_2m`, `solar_radiation`, `pressure_msl`, `cloud_cover` | | `holidays` | `country_code` (opt. `subdivision_code`) | `is_public_holiday`, `is_holiday_eve`, `is_holiday_week` | | `fred` | *(none)* | `fed_funds_rate`, `treasury_10y`, `cpi` | | `air_quality` | `city` | `pm2_5`, `pm10`, `ozone`, `nitrogen_dioxide`, `sulphur_dioxide`, `carbon_monoxide` | | `rates` | *(none — global)* | `breakeven_inflation_10y`, `breakeven_inflation_5y`, `treasury_10y_daily` | | `electricity` | `bidding_zone` (`DE_LU`, `DK_1`, `DK_2`, `GB`) | `load_forecast`, `wind_forecast`, `solar_forecast` | Each enabled variable becomes one extra column named `_` (e.g. `wx_temperature_2m`, `rate_treasury_10y_daily`, `elec_wind_forecast`) and is passed to the model as a known future covariate. ### Example: weather by city ```bash theme={null} curl -X POST https://api.eomer.ai/forecast \ -H "Authorization: Bearer $API_KEY" \ -F "file=@sales_data.csv" \ -F "preset=eomer_pulse" \ -F "prediction_length=30" \ -F 'external_covariates={"weather":{"enabled":true,"source":"cds","city":"copenhagen","variables":["temperature_2m","wind_speed_10m"],"point_in_time":true}}' ``` ### Example: electricity day-ahead forecasts (renewables-driven prices) ```bash theme={null} curl -X POST https://api.eomer.ai/forecast \ -H "Authorization: Bearer $API_KEY" \ -F "file=@prices.csv" \ -F "preset=eomer_horizon_s" \ -F "prediction_length=24" \ -F 'external_covariates={"electricity":{"enabled":true,"bidding_zone":"DK_1","variables":["wind_forecast","solar_forecast","load_forecast"]}}' ``` ### Point-in-time correctness All archive-backed sources (`weather` via CDS when `source="cds"`, `air_quality`, `rates`, `electricity`) store `(reference_time, valid_time)` forecast vintages. With `point_in_time: true` (default) a backtest uses only the forecast that would have been known at each historical origin — no target leakage. The public forecast routes currently use the default `num_val_windows = 1`, which reserves one trailing validation window. Pipeline configurations that explicitly set `num_val_windows = 0` instead use the latest issue to supply known covariates beyond the final observation. > Note: archive-backed sources are scoped to pre-materialized cities/zones — > check `covered_cities` or `covered_bidding_zones` in the catalog. Unknown > cities and zones return `400`. ### Legacy fields (deprecated) The flat form fields `covariates=temperature_2m,...` plus `latitude`/`longitude` are still accepted and internally mapped onto `external_covariates.weather`. Prefer the structured field; do not mix both in one request. ### Error Codes | Code | Condition | | ---- | ------------------------------------------------------------------ | | 400 | Invalid preset name | | 400 | Malformed `external_covariates` JSON or unknown city/zone/variable | | 400 | Both `external_covariates` and legacy `covariates` provided | | 401 | Missing or invalid API key | | 422 | Invalid field values (e.g., `n_series` \< 1) | # Hierarchical Reconciliation Source: https://eomer.mintlify.app/guides/hierarchical-reconciliation Make forecasts add up across regions, stores, and totals ## The problem When your data has a hierarchy — stores inside regions inside a national total — forecasting each series on its own gives you numbers that don't add up. The total forecast rarely equals the sum of the region forecasts, and the regions rarely equal the sum of their stores: ``` Total / \ Region A Region B / \ / \ S1 S2 S3 S4 ``` Reconciliation adjusts the forecasts so every parent equals the sum of its children — a property called *coherence*. Planning, budgeting, and allocation all break down without it, because two teams reading different levels of the same forecast otherwise see different numbers. ## Enabling it Name the columns that form your hierarchy, top to bottom. You do **not** need an id column: the bottom-level series id is derived from the hierarchy values. ```yaml theme={null} reconciliation: enabled: true hierarchy_columns: ["region", "store"] # top → bottom method: min_trace # bottom_up | top_down | min_trace weights: wls_struct # MinT weighting ``` Given rows like `region=north, store=store_1`, the pipeline builds: | Level | Series in the output | | -------------- | ----------------------------------- | | Grand total | `total` | | Region | `north`, `south` | | Store (bottom) | `north/store_1`, `north/store_2`, … | Aggregate series are summed from your data, forecast alongside the bottom series, then reconciled. The output CSV carries every level, so one download serves every audience. ## Choosing a method | Method | Forecasts where | Uses all levels | Best when | | ----------- | --------------- | --------------- | ------------------------------------------------------- | | `bottom_up` | Bottom only | No | Bottom series are long and low-noise | | `top_down` | Total only | No | The aggregate is much cleaner than sparse, noisy leaves | | `min_trace` | Every node | Yes | Almost always — the default | **Bottom-up** trusts the leaves: forecast each store, sum upward. Always coherent, but noise in the leaves accumulates into the total. **Top-down** trusts the root: forecast the total and split it by historical proportions. Very stable, but it assumes those proportions still hold and can't react to a shock at one store. **MinT** (Wickramasuriya et al., 2019) trusts everyone, weighted by their uncertainty. It forecasts every node, then adjusts them all at once to the coherent solution that minimizes total forecast-error variance — the trace of `Var(ỹ − y)`, hence *minimum trace*. Because it uses information from every level, it generally beats both alternatives. ### MinT weights `weights` picks how MinT estimates the error covariance `W`: | Value | `W` | Cost | | ------------ | ------------------------ | ---------------------------------- | | `ols` | Identity | Free | | `wls_struct` | Node degree (default) | Free | | `wls_var` | Per-node error variances | One extra pass per residual window | | `shrink` | Shrunk full covariance | One extra pass per residual window | `wls_var` and `shrink` estimate `W` from real forecast errors, so the pipeline runs an internal backtest first — each window in `residual_windows` costs one extra inference pass. `shrink` is the variant most used in the literature; `wls_struct` is the default here because it needs no extra compute and holds up well. If the series are too short to back-test, the run degrades to `wls_struct` and records a warning rather than failing. ### Top-down allocation When `method: top_down`, `top_down_method` chooses how the total is split: | Value | How shares are computed | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `proportions_of_historical_totals` | Each series' summed history over the summed total (default). Stable; dominated by high-volume periods. | | `average_historical_proportions` | Mean of the per-period shares. Every period counts equally, so small series are tracked better — noisier when the total is near zero. | | `forecast_proportions` | Shares taken from the base forecasts themselves, recursively per level. Adapts to the forecast period at the cost of leaning on noisier disaggregate forecasts. | ## Reading the result Job responses include a `reconciliation` summary: ```json theme={null} { "enabled": true, "method": "min_trace", "weights": "wls_struct", "hierarchy_columns": ["region", "store"], "num_levels": 3, "num_bottom_series": 4, "num_total_series": 7, "coherence_max_abs_error": 5.7e-14, "warnings": [] } ``` `coherence_max_abs_error` is the largest gap between any parent and the sum of its children after reconciliation — it should be at floating-point noise. ## Over the API Pass the same block as a JSON form field. Sending the field at all enables reconciliation, so `enabled` is optional: ```bash theme={null} curl -X POST https://api.eomer.ai/forecast \ -H "Authorization: Bearer $API_KEY" \ -F "file=@sales.csv" \ -F "prediction_length=24" \ -F "presets=eomer_pulse_s" \ -F 'reconciliation={"hierarchy_columns": ["region", "store"], "method": "min_trace"}' ``` The MCP `forecast` tool takes the same JSON in its `reconciliation` parameter. `validate_dataset` and `profile_time_series` report a `hierarchical` flag plus the detected columns, so you can check whether your data supports reconciliation before submitting. ## Limits in this version * **Quantiles are reconciled per column.** Each quantile column is coherent on its own, but the reconciled quantiles are not a coherent joint predictive distribution. * **No covariates.** Reconciliation rejects user-declared and external covariates: an aggregate series has no meaningful value for them. Internal feature engineering still works, since those features are derived per series after the aggregates are built. * **No fine-tuning** in combination with reconciliation. * **Single partition.** A hierarchy can't be split across workers, so a Ray or distributed runtime is forced to one partition. * Hierarchy values must not contain the `separator` (default `/`) or collide with `total_label` — the run fails fast with a message naming the column. # Submit a Forecast Job Source: https://eomer.mintlify.app/guides/submit-job Step-by-step guide - upload CSV, poll status, download results. ## Overview The forecast endpoint is asynchronous: 1. **POST `/forecast`** - upload your data and receive a `job_id` immediately. 2. **GET `/jobs/{job_id}`** - poll until `status` is `completed` or `failed`. 3. **GET `/jobs/{job_id}/download`** - download the forecast CSV. This design lets the API handle long-running forecasts (minutes for large datasets) without blocking the HTTP connection. *** ## Step 1 - Prepare your data ### Required columns | Column | Type | Notes | | ----------- | --------------- | ----------------------------------------- | | `item_id` | string | Series name, e.g. `store_A`, `SKU-123` | | `timestamp` | string/datetime | ISO-8601 preferred: `2024-01-15 09:00:00` | | `target` | float | Observed value at each timestamp | ### Example CSV ```csv theme={null} item_id,timestamp,target store_A,2024-01-01,1500.0 store_A,2024-01-02,1620.0 store_A,2024-01-03,1480.0 store_B,2024-01-01,890.0 store_B,2024-01-02,910.0 store_B,2024-01-03,875.0 ``` Each series must have at least **2 historical observations**. More data improves accuracy (10-100 points per series is a good target). *** ## Step 2 - Submit the job ```bash theme={null} curl -X POST https://eomer-api-cgeu.onrender.com/forecast \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "file=@data.csv" \ -F "prediction_length=7" \ -F "presets=eomer_pulse" \ -F "item_id_column=item_id" \ -F "timestamp_column=timestamp" \ -F "target_column=target" ``` ### Form parameters | Parameter | Default | Description | | ------------------- | ------------- | ------------------------------------------------------------------ | | `file` | - | CSV or XLSX file (**required**) | | `prediction_length` | `24` | Number of future steps to forecast | | `presets` | `eomer_pulse` | Model size (see [model list](/quickstart#available-model-presets)) | | `item_id_column` | `item_id` | Column name for series identifier | | `timestamp_column` | `timestamp` | Column name for datetime | | `target_column` | `target` | Column name for the metric | | `freq` | auto-detected | Pandas frequency string, e.g. `D`, `H`, `W` | ### Response (202 Accepted) ```json theme={null} { "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "status": "pending", "created_at": "2024-01-15T09:00:00+00:00", "prediction_length": 7 } ``` *** ## Step 3 - Poll for completion ```bash theme={null} curl https://eomer-api-cgeu.onrender.com/jobs/3fa85f64-5717-4562-b3fc-2c963f66afa6 \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Status values | Status | Meaning | | ----------- | ------------------------------------------- | | `pending` | Job is queued | | `running` | Forecast is in progress | | `completed` | Forecast is ready to download | | `failed` | An error occurred - check the `error` field | ### Completed response ```json theme={null} { "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "status": "completed", "created_at": "2024-01-15T09:00:00+00:00", "completed_at": "2024-01-15T09:02:15+00:00", "elapsed_seconds": 135.4, "num_items": 2, "prediction_length": 7, "download_url": "/jobs/3fa85f64-5717-4562-b3fc-2c963f66afa6/download" } ``` *** ## Step 4 - Download the forecast ```bash theme={null} curl https://eomer-api-cgeu.onrender.com/jobs/3fa85f64-5717-4562-b3fc-2c963f66afa6/download \ -H "Authorization: Bearer YOUR_API_KEY" \ -o forecast.csv ``` ### Output format ```csv theme={null} item_id,timestamp,mean,0.1,0.5,0.9 store_A,2024-01-08,1560.2,1420.1,1548.9,1698.3 store_A,2024-01-09,1575.8,1433.5,1564.2,1715.7 store_B,2024-01-08,895.1,822.4,892.6,967.8 ``` * `mean` - point forecast * `0.1` / `0.5` / `0.9` - 10th, 50th (median), 90th quantile forecasts *** ## Cleanup Jobs and output files are automatically deleted after 1 hour (configurable via `EOMER_JOB_TTL_SECONDS`). To delete a job early: ```bash theme={null} curl -X DELETE \ https://eomer-api-cgeu.onrender.com/jobs/3fa85f64-5717-4562-b3fc-2c963f66afa6 \ -H "Authorization: Bearer YOUR_API_KEY" ``` Returns `204 No Content` on success. *** ## Python example (end-to-end) ```python theme={null} import time import httpx API_BASE = "https://eomer-api-cgeu.onrender.com" API_KEY = "your-api-key-here" HEADERS = {"Authorization": f"Bearer {API_KEY}"} # 1. Submit with open("data.csv", "rb") as f: resp = httpx.post( f"{API_BASE}/forecast", headers=HEADERS, files={"file": ("data.csv", f, "text/csv")}, data={"prediction_length": "7", "presets": "eomer_pulse"}, ) resp.raise_for_status() job_id = resp.json()["job_id"] print(f"Submitted job: {job_id}") # 2. Poll while True: resp = httpx.get(f"{API_BASE}/jobs/{job_id}", headers=HEADERS) resp.raise_for_status() body = resp.json() print(f"Status: {body['status']}") if body["status"] in ("completed", "failed"): break time.sleep(5) # 3. Download if body["status"] == "completed": resp = httpx.get(f"{API_BASE}/jobs/{job_id}/download", headers=HEADERS) resp.raise_for_status() with open("forecast.csv", "wb") as f: f.write(resp.content) print("Forecast saved to forecast.csv") else: print(f"Job failed: {body.get('error')}") ``` # Tenant-Isolated Storage (Cloudflare R2) Source: https://eomer.mintlify.app/guides/tenant-storage-r2 Secure object uploads/downloads with one bucket per tenant and presigned URLs. ## Why one bucket per tenant Customer datasets are sensitive and must remain strictly isolated.\ This design uses **one private bucket per tenant** to reduce cross-tenant blast radius and make access boundaries explicit for security reviews and audits. Bucket naming convention: `--tenant-` Example: `eomer-production-tenant-acme` ## Isolation model The API enforces tenant isolation at control-plane level: 1. Client authenticates with a Bearer API key. 2. Backend resolves API key -> tenant slug from `EOMER_API_KEY_TENANT_MAP_JSON`. 3. Backend computes tenant bucket name server-side. 4. Backend computes object key server-side (clients cannot choose bucket/key authority). 5. Backend issues short-lived presigned URL. 6. Browser uploads directly to R2 (no long-lived credentials in frontend). 7. Backend persists object metadata and verifies ownership on all follow-up operations. Cross-tenant lookups are denied and return `404` to avoid leaking object existence. ## API endpoints ### `POST /storage/uploads/presign` Returns a short-lived presigned `PUT` URL. Input includes: * `filename` * `content_type` * `file_size` (optional) * `category` (`raw`, `processed`, `exports`, `tmp`) ### `POST /storage/uploads/{object_id}/complete` Marks upload as complete and optionally verifies object existence/size via `HeadObject`. ### `GET /storage/objects/{object_id}/download-url` Returns a short-lived presigned `GET` URL for the tenant-owned object. ### `GET /storage/objects` Lists tenant-owned uploaded object metadata (object ID, filename, content type, size, status, timestamps). ### `GET /storage/objects/{object_id}/preview` Returns a bounded CSV preview payload for a tenant-owned uploaded object: * columns * sample rows * detected timestamp/target/item\_id columns * detected frequency (when inferable) Preview is restricted to `text/csv` objects and read caps are enforced server-side. ### `POST /forecast/storage-object` Submits a forecast job using a tenant-owned `object_id` instead of multipart upload.\ The backend resolves bucket/key server-side, reads the object, and reuses the normal forecast execution path (local or managed compute offload). ## Required environment variables Storage and tenant mapping: * `EOMER_API_KEY_TENANT_MAP_JSON` * `EOMER_STORAGE_PROVIDER` (`r2`) * `EOMER_STORAGE_APP_NAME` * `EOMER_STORAGE_APP_ENV` * `EOMER_STORAGE_PRESIGN_TTL_SECONDS` * `EOMER_STORAGE_ALLOWED_CONTENT_TYPES` (optional) * `EOMER_STORAGE_MAX_UPLOAD_BYTES` (optional) * `EOMER_STORAGE_PREVIEW_MAX_BYTES` (optional, default `262144`) * `EOMER_STORAGE_PREVIEW_MAX_ROWS` (optional, default `100`) Cloudflare R2: * `EOMER_R2_ACCOUNT_ID` * `EOMER_R2_ACCESS_KEY_ID` * `EOMER_R2_SECRET_ACCESS_KEY` * `EOMER_R2_REGION` (typically `auto`) * `EOMER_R2_ENDPOINT_URL` (optional override) ## Cloudflare setup (manual) 1. Create one private bucket per tenant using the naming convention above. 2. Create an R2 access key with least privilege for required bucket operations: * `PutObject` * `GetObject` * `HeadObject` 3. Set API environment variables in deployment platform. 4. Set `EOMER_API_KEY_TENANT_MAP_JSON` for all keys that should access storage. ## Local testing 1. Set env vars from `.env.example` (including a test key -> tenant map entry). 2. Start API: ```bash theme={null} uvicorn eomer_forecasting.api.app:app --host 0.0.0.0 --port 8000 ``` 3. Request presign URL: ```bash theme={null} curl -X POST http://localhost:8000/storage/uploads/presign \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"filename":"data.csv","content_type":"text/csv","file_size":1234,"category":"raw"}' ``` 4. Upload file directly to returned `upload_url` with `PUT`. 5. Call `POST /storage/uploads/{object_id}/complete`. 6. Call `GET /storage/objects/{object_id}/download-url`. ## Automated backend checks Run the complete backend verification suite: ```bash theme={null} scripts/run_backend_storage_checks.sh ``` The script auto-loads `.env` from repo root (if present) before running checks. Optional live smoke-test against deployed API: ```bash theme={null} # Set in .env (or export in shell): # EOMER_SMOKE_API_URL="https://your-api.example.com" # EOMER_SMOKE_API_KEY="your-bearer-key" scripts/run_backend_storage_checks.sh ``` Direct smoke-test only: ```bash theme={null} python scripts/storage_smoke_test.py --url "$EOMER_SMOKE_API_URL" --key "$EOMER_SMOKE_API_KEY" ``` ## Portal integration flow (`www.eomer.ai`) 1. Portal server authenticates the user and resolves the active organization. 2. Portal maps `organizationId -> forecasting API key` via `FORECAST_STORAGE_ORG_API_KEYS_JSON`. 3. Portal requests `POST /storage/uploads/presign`. 4. Browser uploads CSV directly to R2 using presigned `PUT`. 5. Portal finalizes with `POST /storage/uploads/{object_id}/complete`. 6. Portal stores dataset metadata (`Dataset`/`DatasetVersion`) keyed by `storage-object:{object_id}`. 7. For "Connect to database", portal calls `GET /storage/objects` and `GET /storage/objects/{object_id}/preview`. 8. Portal submits jobs with `POST /forecast/storage-object`. ## Portability and migration optionality The API uses an internal object storage interface and an S3-compatible adapter.\ Cloudflare R2 is currently implemented, but migration to AWS S3 or another S3-compatible provider should only require adapter/config changes, not route contract changes. ## Operational notes and current limitations * Runtime does **not** auto-create buckets; provisioning is an explicit ops step. * Presigned upload size limits are enforced at presign request time; the API can also verify size during completion. * Metadata is stored in Redis (with in-memory fallback in local/test mode). * Current upload path categories are fixed: `raw/`, `processed/`, `exports/`, `tmp/`. # Quickstart Source: https://eomer.mintlify.app/quickstart Get your first forecast in under 5 minutes. ## 1. Get your API key Contact the eomer.ai team to receive a Bearer token. Keys are issued per client and can be rotated at any time without downtime. ## 2. Check the API is up ```bash theme={null} curl https://eomer-api-cgeu.onrender.com/health ``` Expected response: ```json theme={null} { "status": "ok", "version": "0.1.0", "timestamp": "2026-01-01T00:00:00+00:00", "checks": { "upload_dir": "ok", "output_dir": "ok" } } ``` ## 3. Submit a forecast job Prepare a CSV file with at minimum three columns: | Column | Description | | ----------- | ---------------------------------- | | `item_id` | Series identifier (e.g. `store_A`) | | `timestamp` | ISO-8601 datetime or date string | | `target` | Numeric value to forecast | ```bash theme={null} curl -X POST https://eomer-api-cgeu.onrender.com/forecast \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "file=@data.csv" \ -F "prediction_length=24" \ -F "presets=eomer_pulse_nano" ``` The API returns immediately with a job ID and status `pending`: ```json theme={null} { "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "status": "pending", "prediction_length": 24 } ``` ## 4. Poll for completion ```bash theme={null} curl https://eomer-api-cgeu.onrender.com/jobs/3fa85f64-5717-4562-b3fc-2c963f66afa6 \ -H "Authorization: Bearer YOUR_API_KEY" ``` When `status` is `completed`, a `download_url` appears in the response. ## 5. Download the forecast ```bash theme={null} curl https://eomer-api-cgeu.onrender.com/jobs/3fa85f64-5717-4562-b3fc-2c963f66afa6/download \ -H "Authorization: Bearer YOUR_API_KEY" \ -o forecast.csv ``` The output CSV contains one row per (item\_id, future\_timestamp) pair with columns `mean`, `0.1`, `0.5`, `0.9` (10th, 50th, 90th quantile forecasts). *** ## Available model presets Retrieve the full list at any time: ```bash theme={null} curl https://eomer-api-cgeu.onrender.com/models \ -H "Authorization: Bearer YOUR_API_KEY" ``` | Preset | Size | Best for | | -------------------- | -------- | --------------------------- | | `eomer_pulse_nano` | Smallest | Fast prototyping, CI | | `eomer_pulse_mini` | Small | Balanced speed and accuracy | | `eomer_pulse` | Medium | Production workloads | | `eomer_horizon_core` | Large | Complex workloads | | `eomer_horizon` | Largest | High-capacity workloads | | `eomer_horizon_max` | Ensemble | Maximum forecast quality | All presets are zero-shot - no training data required.