# Webhook event (https://docs.spavik.co/api-reference/events/event) Structure of the events delivered to your webhook endpoint. OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "webhooks": { "event": { "post": { "x-audience": "client", "description": "POST application/json. Spavik-Signature: t=,v1=. Sign the timestamp, a dot, and the exact raw request bytes with the webhook secret as a UTF-8 string. Use constant-time comparison and reject timestamps more than 300 seconds away from your clock. Each attempt has a fresh signature; event.id and payload remain unchanged. Persist/deduplicate event.id before acknowledging with any 2xx. Duplicate deliveries and unordered events are possible. Default timeout: 5 seconds. Default maximum: 8 attempts; exponential delays from 30 seconds, capped at 1 hour. Network errors, timeouts, HTTP 408, 429 and 5xx are retried. Other non-2xx responses are terminal; redirects are not followed. Retry-After from the receiver is honored up to 24 hours. Scheduling may add delay. These limits are deployment-configurable. Disabling or changing a webhook URL cancels its pending deliveries when processed.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/WebhookEventModelTrained" }, { "$ref": "#/components/schemas/WebhookEventModelRefreshed" }, { "$ref": "#/components/schemas/WebhookEventModelStale" }, { "$ref": "#/components/schemas/WebhookEventCreditsLow" } ], "discriminator": { "propertyName": "type", "mapping": { "model.trained": "#/components/schemas/WebhookEventModelTrained", "model.refreshed": "#/components/schemas/WebhookEventModelRefreshed", "model.stale": "#/components/schemas/WebhookEventModelStale", "credits.low": "#/components/schemas/WebhookEventCreditsLow" } } }, "examples": { "model.trained": { "summary": "model.trained", "value": { "id": "evt_example", "type": "model.trained", "created_at": "2026-10-11T12:00:00.000Z", "workspace_id": "ws_example", "data": { "model_id": "mdl_example", "version": 1, "target": "churn", "task": "classification", "n_train": 100, "metrics": { "AUC": 0.81 } } } }, "model.refreshed": { "summary": "model.refreshed", "value": { "id": "evt_example", "type": "model.refreshed", "created_at": "2026-10-11T12:00:00.000Z", "workspace_id": "ws_example", "data": { "model_id": "mdl_example", "version": 2, "outcomes_applied": 12 } } }, "model.stale": { "summary": "model.stale", "value": { "id": "evt_example", "type": "model.stale", "created_at": "2026-10-11T12:00:00.000Z", "workspace_id": "ws_example", "data": { "model_id": "mdl_example", "pending_outcomes": 50, "hint": "Refresh the model to integrate these outcomes." } } }, "credits.low": { "summary": "credits.low", "value": { "id": "evt_example", "type": "credits.low", "created_at": "2026-10-11T12:00:00.000Z", "workspace_id": "ws_example", "data": { "credit_balance": 10 } } } } } } }, "responses": { "200": { "description": "Event durably accepted or already processed." } } } } } } ``` # Delete a model (https://docs.spavik.co/api-reference/models/delete-v1-models-id) DELETE /v1/models/{id} OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models/{id}": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Model identifier or slug" }, { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "delete": { "x-audience": "client", "tags": [ "Models" ], "summary": "Delete a model for good", "description": "Irreversible: the artefacts are purged from the engine and the pending outcomes go with them. To stop serving a model without losing it, archive it instead. If engine purge fails, the model stops serving and deletion returns 503. Retry DELETE to resume cleanup.", "responses": { "204": { "description": "Deleted" }, "400": { "description": "- `MALFORMED_PATH`: A path segment is not valid percent-encoded text.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_PATH" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "404": { "description": "- `MODEL_NOT_FOUND`: No model with this identifier or slug in the workspace of the key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_NOT_FOUND" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } }, "503": { "description": "- `ENGINE_UNAVAILABLE`: The engine purge could not be confirmed. The model no longer serves; retry DELETE.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_UNAVAILABLE" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Engine families and configured default (https://docs.spavik.co/api-reference/models/get-v1-engine-families) GET /v1/engine-families OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/engine-families": { "parameters": [ { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "get": { "x-audience": "client", "tags": [ "Models" ], "summary": "Engine families and configured default", "security": [], "description": "The family marked is_default is used when engine_family is omitted on model creation, forecasting or backtesting. Existing models retain their original family.", "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "required": [ "id", "label", "is_default" ], "properties": { "id": { "type": "string", "example": "presage" }, "label": { "type": "string", "example": "Presage" }, "is_default": { "type": "boolean" } } } } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } } } } } } } ``` # List engines (https://docs.spavik.co/api-reference/models/get-v1-engines) GET /v1/engines OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/engines": { "parameters": [ { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "get": { "x-audience": "client", "tags": [ "Models" ], "summary": "Available prediction engines", "description": "Choose engine_family (flair, presage, prophecy) at model creation; omission selects the family marked is_default in GET /v1/engine-families. The model remains bound to that family. At prediction time, engine_version accepts a full identifier (e.g. presage-2 or presage-latest): omission or -latest selects the highest active version in that family. Preview and deprecated engines require an explicit version. Retired engines are unavailable.", "security": [], "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "example": "presage-2" }, "family": { "type": "string", "enum": [ "flair", "presage", "prophecy" ], "example": "presage" }, "version": { "type": "string", "example": "2" }, "label": { "type": "string" }, "status": { "type": "string", "enum": [ "default", "preview", "deprecated", "retired" ] }, "description": { "type": "string" }, "tasks": { "type": "array", "items": { "type": "string" }, "example": [ "classification", "regression", "forecast" ] }, "rank": { "type": "integer", "example": 1, "description": "Place in the range, ascending from the lightest to the most capable. The list is already sorted by it: sorting by identifier would show a wrong range." }, "credits": { "type": "integer", "example": 1, "description": "Engine price in credits. A model prediction costs this price per scored row. A forecast call costs the requested horizon times this price, with all series of the call included. Historical rows and quantiles are not billed. The default engine costs 1 credit." }, "min_plan": { "type": "string", "nullable": true, "example": null, "description": "Lowest plan that may declare this engine. Null when open to every plan, Free included." }, "retires_at": { "type": "string", "format": "date", "nullable": true } } } } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } }, "503": { "description": "- `ENGINE_UNAVAILABLE`: The engine did not answer in time, could not be reached, or failed. Retry shortly.\n- `ENGINE_BUSY`: The engine capacity is temporarily exhausted. Retry after Retry-After.\n- `ENGINE_CATALOGUE_MISMATCH`: An offered engine is missing from the engine catalogue. Retry shortly.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_UNAVAILABLE", "ENGINE_BUSY", "ENGINE_CATALOGUE_MISMATCH" ] } } } ] } } } } } } } } } ``` # Get model performance (https://docs.spavik.co/api-reference/models/get-v1-models-id-performance) GET /v1/models/{id}/performance OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models/{id}/performance": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Model identifier or slug" }, { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "get": { "x-audience": "client", "tags": [ "Models" ], "summary": "Real-world performance, against the training metrics", "description": "Compares the original predictions supplied with outcomes to their observed actual values, identically for every plan and data policy. To measure performance, include prediction, version and exact engine with each outcome, and probabilities when available. Inference archives are never consulted. source is client_reported_predictions: these measurements are client-reported, not independently verified. quality_reports exposes aggregate batch diagnostics; small-sample quality scores are null. `observed` stays null until an outcome includes a readable original prediction and its version and engine. Probability scores require client-supplied class probabilities for every compared row and compatible class orders. Missing or invalid probabilities produce null scores with reasons; label-based scores remain available. References use training parameters for each prediction version, scored on these same outcomes. No reference is fitted to the outcomes. Old versions without saved reference parameters have null baselines.", "parameters": [ { "name": "days", "in": "query", "schema": { "type": "integer", "default": 30, "maximum": 365 } } ], "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "type": "object", "properties": { "source": { "type": "string", "enum": [ "client_reported_predictions" ] }, "quality_reports": { "type": "array", "items": { "type": "object", "properties": { "version": { "type": "integer", "nullable": true }, "engine": { "type": "string", "nullable": true }, "created_at": { "type": "string", "format": "date-time" }, "report": { "type": "object", "additionalProperties": true } } } }, "model_id": { "type": "string", "example": "mdl_7Kq2Vn5RtWz3Bx8Ldm" }, "version": { "type": "integer" }, "task": { "type": "string", "enum": [ "classification", "regression" ] }, "pending_outcomes": { "type": "integer" }, "refresh_suggested": { "type": "boolean", "description": "True when pending outcomes reached the suggestion threshold (50 by default). Call POST /v1/models/{id}/refresh to fold them in." }, "training_engine": { "type": "string", "nullable": true }, "training_metrics": { "type": "object", "nullable": true, "additionalProperties": { "type": "number", "nullable": true }, "description": "Scores on evaluated observations. Classification: AUC, Accuracy, AveragePrecision (binary only), LogLoss and MacroF1. Regression: MAE, RMSE, R². Forecasting adds MASE, Bias, BiasRatio and Pinball. Undefined scores are null; see metric_context.undefined_reasons. Secondary scores do not change the primary assessment.", "properties": { "AUC": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "ROC AUC, binary positive class or multiclass one-vs-rest macro average." }, "Accuracy": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1 }, "AveragePrecision": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Binary non-interpolated average precision, not trapezoidal PR-AUC. Null without positive observations." }, "LogLoss": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean negative log probability of the actual class, natural logarithm, float64 epsilon clipping. Lower is better." }, "MacroF1": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Unweighted mean of defined per-class F1 scores. Classes absent from both actual and predicted labels are excluded." }, "MAE": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean absolute error in target units." }, "RMSE": { "type": "number", "nullable": true, "minimum": 0 }, "R²": { "type": "number", "nullable": true, "description": "Null for fewer than two observations or constant actual values. Can be negative." }, "MASE": { "type": "number", "nullable": true, "minimum": 0, "description": "Compare with the scored naive baseline on the same backtest, not with a fixed threshold of 1." }, "Bias": { "type": "number", "nullable": true }, "BiasRatio": { "type": "number", "nullable": true }, "Pinball": { "type": "number", "nullable": true, "minimum": 0 } } }, "training_evaluation": { "type": "object", "nullable": true, "description": "Engine evaluation metadata, forwarded without recalculating the split. Null when not supplied by the engine. Row positions are zero-based in the received data, excluding its header and before dropping missing targets.", "properties": { "metric_context": { "$ref": "#/components/schemas/MetricContext" }, "protocol_version": { "type": "integer", "example": 2 }, "interpretation": { "type": "string", "enum": [ "descriptive_only" ] }, "kind": { "type": "string", "enum": [ "random_holdout" ] }, "split": { "type": "object", "properties": { "seed": { "type": "integer" }, "shuffle": { "type": "boolean" }, "test_fraction_requested": { "type": "number" }, "stratified": { "type": "boolean" }, "stratification_reason": { "type": "string" }, "train_rows": { "type": "array", "items": { "type": "integer", "minimum": 0 } }, "test_rows": { "type": "array", "items": { "type": "integer", "minimum": 0 } } } }, "sampling": { "type": "object", "properties": { "method": { "type": "string", "enum": [ "none", "uniform_without_replacement" ] }, "seed": { "type": "integer" }, "n_input": { "type": "integer", "minimum": 0 }, "n_missing_target": { "type": "integer", "minimum": 0 }, "n_eligible": { "type": "integer", "minimum": 0 }, "n_selected": { "type": "integer", "minimum": 0 }, "max_samples_requested": { "type": "integer", "minimum": 0 }, "effective_cap": { "type": "integer", "minimum": 0 } } }, "class_counts": { "type": "object", "nullable": true, "properties": { "eligible": { "type": "array", "items": { "type": "object", "properties": { "class": { "type": "string" }, "count": { "type": "integer", "minimum": 0 } } } }, "sampled": { "type": "array", "items": { "type": "object", "properties": { "class": { "type": "string" }, "count": { "type": "integer", "minimum": 0 } } } }, "train": { "type": "array", "items": { "type": "object", "properties": { "class": { "type": "string" }, "count": { "type": "integer", "minimum": 0 } } } }, "test": { "type": "array", "items": { "type": "object", "properties": { "class": { "type": "string" }, "count": { "type": "integer", "minimum": 0 } } } } } }, "auc": { "type": "object", "nullable": true, "properties": { "variant": { "type": "string", "description": "Binary ROC AUC or multiclass one-vs-rest with an unweighted macro average." }, "class_order": { "type": "array", "items": { "type": "string" } }, "positive_class": { "type": "string", "nullable": true }, "available": { "type": "boolean" }, "unavailable_reason": { "type": "string", "nullable": true } } } } }, "evaluation": { "type": "object", "properties": { "kind": { "type": "string", "enum": [ "matched_outcomes" ] }, "since": { "type": "string", "format": "date-time" }, "limit": { "type": "integer", "minimum": 0 }, "fetched_count": { "type": "integer", "minimum": 0 }, "readable_count": { "type": "integer", "minimum": 0 }, "evaluated_count": { "type": "integer", "minimum": 0 }, "limit_reached": { "type": "boolean" } } }, "training_classification_report": { "$ref": "#/components/schemas/ClassificationReport" }, "training_calibration": { "$ref": "#/components/schemas/Calibration" }, "observed": { "type": "object", "nullable": true, "description": "Measure suited to the task. For classification, `metric` is accuracy and `value` the share of exact answers (higher is better). For regression, `metric` is mean_absolute_error and `value` the average gap between predicted and actual (lower is better). `baseline` is what the training reference scores on the same outcomes, using each prediction version. Missing training parameters give a null baseline. Read metrics, baselines, metric_context and assessment for the complete task-specific evaluation.", "properties": { "metric": { "type": "string", "enum": [ "accuracy", "mean_absolute_error" ] }, "compared": { "type": "integer" }, "value": { "type": "number", "nullable": true }, "baseline": { "type": "number", "nullable": true }, "better_than_baseline": { "type": "boolean", "nullable": true }, "classification_report": { "$ref": "#/components/schemas/ClassificationReport" }, "metrics": { "type": "object", "nullable": true, "additionalProperties": { "type": "number", "nullable": true }, "description": "Scores on evaluated observations. Classification: AUC, Accuracy, AveragePrecision (binary only), LogLoss and MacroF1. Regression: MAE, RMSE, R². Forecasting adds MASE, Bias, BiasRatio and Pinball. Undefined scores are null; see metric_context.undefined_reasons. Secondary scores do not change the primary assessment.", "properties": { "AUC": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "ROC AUC, binary positive class or multiclass one-vs-rest macro average." }, "Accuracy": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1 }, "AveragePrecision": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Binary non-interpolated average precision, not trapezoidal PR-AUC. Null without positive observations." }, "LogLoss": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean negative log probability of the actual class, natural logarithm, float64 epsilon clipping. Lower is better." }, "MacroF1": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Unweighted mean of defined per-class F1 scores. Classes absent from both actual and predicted labels are excluded." }, "MAE": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean absolute error in target units." }, "RMSE": { "type": "number", "nullable": true, "minimum": 0 }, "R²": { "type": "number", "nullable": true, "description": "Null for fewer than two observations or constant actual values. Can be negative." }, "MASE": { "type": "number", "nullable": true, "minimum": 0, "description": "Compare with the scored naive baseline on the same backtest, not with a fixed threshold of 1." }, "Bias": { "type": "number", "nullable": true }, "BiasRatio": { "type": "number", "nullable": true }, "Pinball": { "type": "number", "nullable": true, "minimum": 0 } } }, "metric_context": { "$ref": "#/components/schemas/MetricContext" }, "baselines": { "type": "object", "nullable": true, "description": "References indexed by metric. Read floor for the numeric score, not the whole object. Fit provenance must come from the engine; a missing fit_on does not mean train.", "additionalProperties": { "type": "object", "properties": { "floor": { "type": "number", "nullable": true }, "class_order": { "type": "array", "nullable": true, "items": { "type": "string" } }, "probabilities": { "type": "array", "nullable": true, "items": { "type": "number", "minimum": 0, "maximum": 1 } }, "undefined_reason": { "type": "string", "nullable": true }, "relative_error_reduction": { "type": "number", "nullable": true, "description": "For observed loss metrics only: 1 - model score / baseline score on identical outcomes. Fraction, possibly negative. Null for non-loss metrics, zero baseline or unavailable scores." }, "reduction_undefined_reason": { "type": "string", "nullable": true }, "label": { "type": "string" }, "higher_better": { "type": "boolean" }, "fit_on": { "type": "string", "nullable": true, "enum": [ "train", "test", "theoretical", null ], "description": "Where reference parameters were determined. MAE comparison requires train." }, "prediction": { "type": [ "number", "string", "null" ], "description": "Constant prediction fitted on training data, or null for a probability vector or forecasting reference." }, "constant_floor": { "type": "number", "nullable": true, "description": "MASE of the historical-median reference on the same evaluation points." }, "constant_label": { "type": "string", "nullable": true } } } }, "assessment": { "type": "object", "additionalProperties": false, "required": [ "status", "metric", "message" ], "properties": { "status": { "type": "string", "description": "Display better_than_baseline as Better than the reference on this sample, alongside assessment.message and the evaluated observation count. A dataset without signal can beat a baseline by chance. Do not present this as a reliability or deployment approval.", "enum": [ "better_than_baseline", "not_better_than_baseline", "inconclusive", "not_evaluated" ] }, "metric": { "type": "string", "enum": [ "AUC", "MAE", "MASE" ] }, "message": { "type": "string" } }, "description": "The single API conclusion, always present and non-null in HTTP 200 responses, including when the trial cannot run. Forward status, metric and message without recomputing them from scores. An absent or null assessment is a contract violation, not an inconclusive result. This is a descriptive comparison on the evaluated observations: better means strictly better than every required reference; equality is not better. No fixed gain or sample-size threshold implies significance. Inconclusive means missing or invalid evidence, or a skipped backtest; not_evaluated means validation prevented the trial. No status establishes reliability, future performance or absence of predictive signal." }, "by_engine": { "type": "array", "description": "Observed scores grouped by data version and exact engine; historical events inherit their preparation engine.", "items": { "type": "object", "properties": { "version": { "type": "integer" }, "engine": { "type": "string", "nullable": true }, "compared": { "type": "integer" }, "metrics": { "type": "object", "nullable": true, "additionalProperties": { "type": "number", "nullable": true }, "description": "Scores on evaluated observations. Classification: AUC, Accuracy, AveragePrecision (binary only), LogLoss and MacroF1. Regression: MAE, RMSE, R². Forecasting adds MASE, Bias, BiasRatio and Pinball. Undefined scores are null; see metric_context.undefined_reasons. Secondary scores do not change the primary assessment.", "properties": { "AUC": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "ROC AUC, binary positive class or multiclass one-vs-rest macro average." }, "Accuracy": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1 }, "AveragePrecision": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Binary non-interpolated average precision, not trapezoidal PR-AUC. Null without positive observations." }, "LogLoss": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean negative log probability of the actual class, natural logarithm, float64 epsilon clipping. Lower is better." }, "MacroF1": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Unweighted mean of defined per-class F1 scores. Classes absent from both actual and predicted labels are excluded." }, "MAE": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean absolute error in target units." }, "RMSE": { "type": "number", "nullable": true, "minimum": 0 }, "R²": { "type": "number", "nullable": true, "description": "Null for fewer than two observations or constant actual values. Can be negative." }, "MASE": { "type": "number", "nullable": true, "minimum": 0, "description": "Compare with the scored naive baseline on the same backtest, not with a fixed threshold of 1." }, "Bias": { "type": "number", "nullable": true }, "BiasRatio": { "type": "number", "nullable": true }, "Pinball": { "type": "number", "nullable": true, "minimum": 0 } } }, "metric_context": { "$ref": "#/components/schemas/MetricContext" }, "baselines": { "type": "object", "nullable": true, "description": "References indexed by metric. Read floor for the numeric score, not the whole object. Fit provenance must come from the engine; a missing fit_on does not mean train.", "additionalProperties": { "type": "object", "properties": { "floor": { "type": "number", "nullable": true }, "class_order": { "type": "array", "nullable": true, "items": { "type": "string" } }, "probabilities": { "type": "array", "nullable": true, "items": { "type": "number", "minimum": 0, "maximum": 1 } }, "undefined_reason": { "type": "string", "nullable": true }, "relative_error_reduction": { "type": "number", "nullable": true, "description": "For observed loss metrics only: 1 - model score / baseline score on identical outcomes. Fraction, possibly negative. Null for non-loss metrics, zero baseline or unavailable scores." }, "reduction_undefined_reason": { "type": "string", "nullable": true }, "label": { "type": "string" }, "higher_better": { "type": "boolean" }, "fit_on": { "type": "string", "nullable": true, "enum": [ "train", "test", "theoretical", null ], "description": "Where reference parameters were determined. MAE comparison requires train." }, "prediction": { "type": [ "number", "string", "null" ], "description": "Constant prediction fitted on training data, or null for a probability vector or forecasting reference." }, "constant_floor": { "type": "number", "nullable": true, "description": "MASE of the historical-median reference on the same evaluation points." }, "constant_label": { "type": "string", "nullable": true } } } }, "assessment": { "type": "object", "additionalProperties": false, "required": [ "status", "metric", "message" ], "properties": { "status": { "type": "string", "description": "Display better_than_baseline as Better than the reference on this sample, alongside assessment.message and the evaluated observation count. A dataset without signal can beat a baseline by chance. Do not present this as a reliability or deployment approval.", "enum": [ "better_than_baseline", "not_better_than_baseline", "inconclusive", "not_evaluated" ] }, "metric": { "type": "string", "enum": [ "AUC", "MAE", "MASE" ] }, "message": { "type": "string" } }, "description": "The single API conclusion, always present and non-null in HTTP 200 responses, including when the trial cannot run. Forward status, metric and message without recomputing them from scores. An absent or null assessment is a contract violation, not an inconclusive result. This is a descriptive comparison on the evaluated observations: better means strictly better than every required reference; equality is not better. No fixed gain or sample-size threshold implies significance. Inconclusive means missing or invalid evidence, or a skipped backtest; not_evaluated means validation prevented the trial. No status establishes reliability, future performance or absence of predictive signal." }, "classification_report": { "$ref": "#/components/schemas/ClassificationReport" } } } }, "by_version": { "type": "array", "items": { "type": "object", "properties": { "version": { "type": "integer" }, "compared": { "type": "integer", "minimum": 0 }, "value": { "type": [ "number", "null" ] }, "classification_report": { "$ref": "#/components/schemas/ClassificationReport" }, "metrics": { "type": "object", "nullable": true, "additionalProperties": { "type": "number", "nullable": true }, "description": "Scores on evaluated observations. Classification: AUC, Accuracy, AveragePrecision (binary only), LogLoss and MacroF1. Regression: MAE, RMSE, R². Forecasting adds MASE, Bias, BiasRatio and Pinball. Undefined scores are null; see metric_context.undefined_reasons. Secondary scores do not change the primary assessment.", "properties": { "AUC": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "ROC AUC, binary positive class or multiclass one-vs-rest macro average." }, "Accuracy": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1 }, "AveragePrecision": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Binary non-interpolated average precision, not trapezoidal PR-AUC. Null without positive observations." }, "LogLoss": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean negative log probability of the actual class, natural logarithm, float64 epsilon clipping. Lower is better." }, "MacroF1": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Unweighted mean of defined per-class F1 scores. Classes absent from both actual and predicted labels are excluded." }, "MAE": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean absolute error in target units." }, "RMSE": { "type": "number", "nullable": true, "minimum": 0 }, "R²": { "type": "number", "nullable": true, "description": "Null for fewer than two observations or constant actual values. Can be negative." }, "MASE": { "type": "number", "nullable": true, "minimum": 0, "description": "Compare with the scored naive baseline on the same backtest, not with a fixed threshold of 1." }, "Bias": { "type": "number", "nullable": true }, "BiasRatio": { "type": "number", "nullable": true }, "Pinball": { "type": "number", "nullable": true, "minimum": 0 } } }, "metric_context": { "$ref": "#/components/schemas/MetricContext" }, "baselines": { "type": "object", "nullable": true, "description": "References indexed by metric. Read floor for the numeric score, not the whole object. Fit provenance must come from the engine; a missing fit_on does not mean train.", "additionalProperties": { "type": "object", "properties": { "floor": { "type": "number", "nullable": true }, "class_order": { "type": "array", "nullable": true, "items": { "type": "string" } }, "probabilities": { "type": "array", "nullable": true, "items": { "type": "number", "minimum": 0, "maximum": 1 } }, "undefined_reason": { "type": "string", "nullable": true }, "relative_error_reduction": { "type": "number", "nullable": true, "description": "For observed loss metrics only: 1 - model score / baseline score on identical outcomes. Fraction, possibly negative. Null for non-loss metrics, zero baseline or unavailable scores." }, "reduction_undefined_reason": { "type": "string", "nullable": true }, "label": { "type": "string" }, "higher_better": { "type": "boolean" }, "fit_on": { "type": "string", "nullable": true, "enum": [ "train", "test", "theoretical", null ], "description": "Where reference parameters were determined. MAE comparison requires train." }, "prediction": { "type": [ "number", "string", "null" ], "description": "Constant prediction fitted on training data, or null for a probability vector or forecasting reference." }, "constant_floor": { "type": "number", "nullable": true, "description": "MASE of the historical-median reference on the same evaluation points." }, "constant_label": { "type": "string", "nullable": true } } } }, "assessment": { "type": "object", "additionalProperties": false, "required": [ "status", "metric", "message" ], "properties": { "status": { "type": "string", "description": "Display better_than_baseline as Better than the reference on this sample, alongside assessment.message and the evaluated observation count. A dataset without signal can beat a baseline by chance. Do not present this as a reliability or deployment approval.", "enum": [ "better_than_baseline", "not_better_than_baseline", "inconclusive", "not_evaluated" ] }, "metric": { "type": "string", "enum": [ "AUC", "MAE", "MASE" ] }, "message": { "type": "string" } }, "description": "The single API conclusion, always present and non-null in HTTP 200 responses, including when the trial cannot run. Forward status, metric and message without recomputing them from scores. An absent or null assessment is a contract violation, not an inconclusive result. This is a descriptive comparison on the evaluated observations: better means strictly better than every required reference; equality is not better. No fixed gain or sample-size threshold implies significance. Inconclusive means missing or invalid evidence, or a skipped backtest; not_evaluated means validation prevented the trial. No status establishes reliability, future performance or absence of predictive signal." } } } } } } } } } } }, "400": { "description": "- `MALFORMED_PATH`: A path segment is not valid percent-encoded text.\n- `VALIDATION_ERROR`: A query parameter is invalid; messageDetail names it.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_PATH", "VALIDATION_ERROR" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "404": { "description": "- `MODEL_NOT_FOUND`: No model with this identifier or slug in the workspace of the key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_NOT_FOUND" ] } } } ] } } } }, "409": { "description": "- `MODEL_ARCHIVED`: The model is archived. Restore it first.\n- `MODEL_NOT_TRAINED`: The model has no version able to serve yet.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_ARCHIVED", "MODEL_NOT_TRAINED" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } }, "503": { "description": "- `PERFORMANCE_UNAVAILABLE`: The outcome log is temporarily unreadable. Retry shortly.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "PERFORMANCE_UNAVAILABLE" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # List model versions (https://docs.spavik.co/api-reference/models/get-v1-models-id-versions) GET /v1/models/{id}/versions OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models/{id}/versions": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Model identifier or slug" }, { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "get": { "x-audience": "client", "tags": [ "Models" ], "summary": "List versions", "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ModelVersion" } } } } }, "400": { "description": "- `MALFORMED_PATH`: A path segment is not valid percent-encoded text.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_PATH" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "404": { "description": "- `MODEL_NOT_FOUND`: No model with this identifier or slug in the workspace of the key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_NOT_FOUND" ] } } } ] } } } }, "409": { "description": "- `MODEL_ARCHIVED`: The model is archived. Restore it first.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_ARCHIVED" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Get a model (https://docs.spavik.co/api-reference/models/get-v1-models-id) GET /v1/models/{id} OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models/{id}": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Model identifier or slug" }, { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "get": { "x-audience": "client", "tags": [ "Models" ], "summary": "Model details", "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Model" } } } }, "400": { "description": "- `MALFORMED_PATH`: A path segment is not valid percent-encoded text.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_PATH" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "404": { "description": "- `MODEL_NOT_FOUND`: No model with this identifier or slug in the workspace of the key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_NOT_FOUND" ] } } } ] } } } }, "409": { "description": "- `MODEL_ARCHIVED`: The model is archived. Restore it first.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_ARCHIVED" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # List models (https://docs.spavik.co/api-reference/models/get-v1-models) GET /v1/models OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models": { "parameters": [ { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "get": { "x-audience": "client", "tags": [ "Models" ], "summary": "List the workspace models", "parameters": [ { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } }, { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 } }, { "name": "status", "in": "query", "schema": { "type": "string", "enum": [ "archived" ] }, "description": "Serving models by default. `archived` lists what was set aside, so it can be found again and restored. Deleted models are never listed." } ], "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Model" } }, "pagination": { "$ref": "#/components/schemas/Pagination" } } } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Rename a model (https://docs.spavik.co/api-reference/models/patch-v1-models-id) PATCH /v1/models/{id} OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models/{id}": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Model identifier or slug" }, { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "patch": { "x-audience": "client", "tags": [ "Models" ], "summary": "Update a model and its automatic refresh preferences", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string" }, "auto_update": { "type": "boolean" }, "retain_outcomes": { "type": "boolean", "deprecated": true, "description": "Deprecated and ignored. Outcomes and refresh are always available." } }, "required": [] } } } }, "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Model" } } } }, "400": { "description": "- `MALFORMED_JSON`: The request body is not valid JSON.\n- `REQUEST_ABORTED`: The request ended before its body was fully received.\n- `BAD_REQUEST`: The request headers or body could not be read.\n- `MALFORMED_PATH`: A path segment is not valid percent-encoded text.\n- `VALIDATION_ERROR`: The body is empty or a field is invalid; messageDetail names it.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_JSON", "REQUEST_ABORTED", "BAD_REQUEST", "MALFORMED_PATH", "VALIDATION_ERROR" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "404": { "description": "- `MODEL_NOT_FOUND`: No model with this identifier or slug in the workspace of the key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_NOT_FOUND" ] } } } ] } } } }, "409": { "description": "- `MODEL_ARCHIVED`: The model is archived. Restore it first.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_ARCHIVED" ] } } } ] } } } }, "413": { "description": "- `PAYLOAD_TOO_LARGE`: The JSON body is too large. Send the data as a CSV file (field: file).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "PAYLOAD_TOO_LARGE" ] } } } ] } } } }, "415": { "description": "- `UNSUPPORTED_CHARSET`: The body charset is not supported. Send UTF-8.\n- `UNSUPPORTED_ENCODING`: The Content-Encoding is not supported. Send the body uncompressed or gzipped.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "UNSUPPORTED_CHARSET", "UNSUPPORTED_ENCODING" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Evaluate a dataset (https://docs.spavik.co/api-reference/models/post-v1-datasets-evaluate) POST /v1/datasets/evaluate OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/datasets/evaluate": { "parameters": [ { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "post": { "x-audience": "client", "tags": [ "Models" ], "summary": "Evaluate a table before training (free, creates nothing)", "description": "Call this before POST /v1/models, on the same table: it is the free check that tells whether the data can be learned, without creating a model or taking a slot in the plan quota. Tables are limited to limits.max_feature_columns feature columns (target excluded, see GET /v1/plans), the same limit as training; with explain=true, trial.feature_importance ranks them to help choose. Creates no model and charges nothing. Returns structural validation and an unpersisted engine trial. Input and response retention follows the account data policy; aggregate quality telemetry is retained for up to 90 days. Every HTTP 200 response includes a non-null assessment (status, metric, message). The single assessment describes an observed comparison, not statistical significance or future reliability. Classification uses AUC as the primary discrimination metric, compared with its reference (normally 0.5). Accuracy remains a secondary decision metric; it cannot replace missing AUC or override its comparison. Regression uses MAE and requires baselines.MAE.fit_on = train. Older engines that compute a mean on test labels remain inconclusive until that reference is corrected.\n\nA strict improvement gives better_than_baseline, equality or worse gives not_better_than_baseline. Missing or invalid primary score, required reference or n_test gives inconclusive. No fixed 0.02 margin or 30-observation threshold is used. Small tests report their actual count and only an observed comparison. n_sampled_from is the engine count before sampling, after dropping missing targets; n_rows is the input count. The current engine uses a random holdout; the API cannot verify independence of people or periods. trial.evaluation exposes the engine protocol, original train/test row positions, sampling counts, class counts and the AUC definition or unavailability reason. It is null when not supplied. High accuracy, exact target copies and duplicate rows do not change the numeric comparison; structural warnings separately ask the caller to verify feature availability and independence.\n\nWhen validation fails, trial is null and assessment.status is not_evaluated. Forward assessment unchanged; an absent or null assessment is a contract violation, not inconclusive. Use POST /v1/forecast/backtest for time series.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "data", "target" ], "properties": { "target": { "type": "string" }, "task": { "type": "string", "enum": [ "classification", "regression" ], "description": "Inferred from the target values when omitted." }, "max_samples": { "type": "integer", "minimum": 1, "description": "Context rows requested for the trial. Capped by the plan (limits.max_context_rows in GET /v1/plans), never refused." }, "explain": { "type": "boolean", "description": "Also rank every feature column by how much the trial score drops when its values are shuffled (trial.feature_importance). Off by default: it runs one extra prediction per column, so the call takes noticeably longer on wide tables. Use it to choose which columns to keep." }, "data": { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "Data rows, one object per row." } } }, "example": { "target": "churn", "data": [ { "plan": "pro", "seats": 12, "tickets_90d": 3, "churn": 0 }, { "plan": "free", "seats": 1, "tickets_90d": 9, "churn": 1 }, { "plan": "pro", "seats": 40, "tickets_90d": 0, "churn": 0 }, { "plan": "free", "seats": 2, "tickets_90d": 7, "churn": 1 }, { "plan": "growth", "seats": 120, "tickets_90d": 1, "churn": 0 }, { "plan": "free", "seats": 1, "tickets_90d": 12, "churn": 1 }, { "plan": "pro", "seats": 25, "tickets_90d": 2, "churn": 0 }, { "plan": "free", "seats": 3, "tickets_90d": 5, "churn": 1 }, { "plan": "growth", "seats": 80, "tickets_90d": 4, "churn": 0 }, { "plan": "pro", "seats": 8, "tickets_90d": 11, "churn": 1 }, { "plan": "growth", "seats": 200, "tickets_90d": 0, "churn": 0 }, { "plan": "free", "seats": 4, "tickets_90d": 2, "churn": 0 } ] } }, "multipart/form-data": { "schema": { "type": "object", "required": [ "file", "target" ], "properties": { "file": { "type": "string", "format": "binary", "description": "Labelled history (CSV)." }, "target": { "type": "string" }, "task": { "type": "string", "enum": [ "classification", "regression" ], "description": "Inferred from the target values when omitted." }, "max_samples": { "type": "integer", "minimum": 1, "description": "Context rows requested for the trial. Capped by the plan (limits.max_context_rows in GET /v1/plans), never refused." }, "explain": { "type": "boolean", "description": "Also rank every feature column by how much the trial score drops when its values are shuffled (trial.feature_importance). Off by default: it runs one extra prediction per column, so the call takes noticeably longer on wide tables. Use it to choose which columns to keep." } } } } } }, "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "type": "object", "required": [ "assessment" ], "properties": { "target": { "type": "string" }, "task": { "type": "string", "enum": [ "classification", "regression" ] }, "n_rows": { "type": "integer" }, "columns": { "type": "array", "items": { "type": "string" } }, "validation": { "type": "object", "additionalProperties": true }, "assessment": { "type": "object", "additionalProperties": false, "required": [ "status", "metric", "message" ], "properties": { "status": { "type": "string", "description": "Display better_than_baseline as Better than the reference on this sample, alongside assessment.message and the evaluated observation count. A dataset without signal can beat a baseline by chance. Do not present this as a reliability or deployment approval.", "enum": [ "better_than_baseline", "not_better_than_baseline", "inconclusive", "not_evaluated" ] }, "metric": { "type": "string", "enum": [ "AUC", "MAE", "MASE" ] }, "message": { "type": "string" } }, "description": "The single API conclusion, always present and non-null in HTTP 200 responses, including when the trial cannot run. Forward status, metric and message without recomputing them from scores. An absent or null assessment is a contract violation, not an inconclusive result. This is a descriptive comparison on the evaluated observations: better means strictly better than every required reference; equality is not better. No fixed gain or sample-size threshold implies significance. Inconclusive means missing or invalid evidence, or a skipped backtest; not_evaluated means validation prevented the trial. No status establishes reliability, future performance or absence of predictive signal." }, "hint": { "type": "string", "nullable": true, "description": "The first thing to fix, taken from the most serious issue." }, "trial": { "type": "object", "nullable": true, "additionalProperties": true, "description": "What the engine makes of the data. Null when validation failed.", "properties": { "engine": { "type": "string", "nullable": true, "description": "Exact engine that performed the evaluation." }, "metrics": { "type": "object", "nullable": true, "additionalProperties": { "type": "number", "nullable": true }, "description": "Scores on evaluated observations. Classification: AUC, Accuracy, AveragePrecision (binary only), LogLoss and MacroF1. Regression: MAE, RMSE, R². Forecasting adds MASE, Bias, BiasRatio and Pinball. Undefined scores are null; see metric_context.undefined_reasons. Secondary scores do not change the primary assessment.", "properties": { "AUC": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "ROC AUC, binary positive class or multiclass one-vs-rest macro average." }, "Accuracy": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1 }, "AveragePrecision": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Binary non-interpolated average precision, not trapezoidal PR-AUC. Null without positive observations." }, "LogLoss": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean negative log probability of the actual class, natural logarithm, float64 epsilon clipping. Lower is better." }, "MacroF1": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Unweighted mean of defined per-class F1 scores. Classes absent from both actual and predicted labels are excluded." }, "MAE": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean absolute error in target units." }, "RMSE": { "type": "number", "nullable": true, "minimum": 0 }, "R²": { "type": "number", "nullable": true, "description": "Null for fewer than two observations or constant actual values. Can be negative." }, "MASE": { "type": "number", "nullable": true, "minimum": 0, "description": "Compare with the scored naive baseline on the same backtest, not with a fixed threshold of 1." }, "Bias": { "type": "number", "nullable": true }, "BiasRatio": { "type": "number", "nullable": true }, "Pinball": { "type": "number", "nullable": true, "minimum": 0 } } }, "classification_report": { "$ref": "#/components/schemas/ClassificationReport" }, "calibration": { "$ref": "#/components/schemas/Calibration" }, "baselines": { "type": "object", "nullable": true, "description": "References indexed by metric. Read floor for the numeric score, not the whole object. Fit provenance must come from the engine; a missing fit_on does not mean train.", "additionalProperties": { "type": "object", "properties": { "floor": { "type": "number", "nullable": true }, "class_order": { "type": "array", "nullable": true, "items": { "type": "string" } }, "probabilities": { "type": "array", "nullable": true, "items": { "type": "number", "minimum": 0, "maximum": 1 } }, "undefined_reason": { "type": "string", "nullable": true }, "relative_error_reduction": { "type": "number", "nullable": true, "description": "For observed loss metrics only: 1 - model score / baseline score on identical outcomes. Fraction, possibly negative. Null for non-loss metrics, zero baseline or unavailable scores." }, "reduction_undefined_reason": { "type": "string", "nullable": true }, "label": { "type": "string" }, "higher_better": { "type": "boolean" }, "fit_on": { "type": "string", "nullable": true, "enum": [ "train", "test", "theoretical", null ], "description": "Where reference parameters were determined. MAE comparison requires train." }, "prediction": { "type": [ "number", "string", "null" ], "description": "Constant prediction fitted on training data, or null for a probability vector or forecasting reference." }, "constant_floor": { "type": "number", "nullable": true, "description": "MASE of the historical-median reference on the same evaluation points." }, "constant_label": { "type": "string", "nullable": true } } } }, "evaluation": { "type": "object", "nullable": true, "description": "Engine evaluation metadata, forwarded without recalculating the split. Null when not supplied by the engine. Row positions are zero-based in the received data, excluding its header and before dropping missing targets.", "properties": { "metric_context": { "$ref": "#/components/schemas/MetricContext" }, "protocol_version": { "type": "integer", "example": 2 }, "interpretation": { "type": "string", "enum": [ "descriptive_only" ] }, "kind": { "type": "string", "enum": [ "random_holdout" ] }, "split": { "type": "object", "properties": { "seed": { "type": "integer" }, "shuffle": { "type": "boolean" }, "test_fraction_requested": { "type": "number" }, "stratified": { "type": "boolean" }, "stratification_reason": { "type": "string" }, "train_rows": { "type": "array", "items": { "type": "integer", "minimum": 0 } }, "test_rows": { "type": "array", "items": { "type": "integer", "minimum": 0 } } } }, "sampling": { "type": "object", "properties": { "method": { "type": "string", "enum": [ "none", "uniform_without_replacement" ] }, "seed": { "type": "integer" }, "n_input": { "type": "integer", "minimum": 0 }, "n_missing_target": { "type": "integer", "minimum": 0 }, "n_eligible": { "type": "integer", "minimum": 0 }, "n_selected": { "type": "integer", "minimum": 0 }, "max_samples_requested": { "type": "integer", "minimum": 0 }, "effective_cap": { "type": "integer", "minimum": 0 } } }, "class_counts": { "type": "object", "nullable": true, "properties": { "eligible": { "type": "array", "items": { "type": "object", "properties": { "class": { "type": "string" }, "count": { "type": "integer", "minimum": 0 } } } }, "sampled": { "type": "array", "items": { "type": "object", "properties": { "class": { "type": "string" }, "count": { "type": "integer", "minimum": 0 } } } }, "train": { "type": "array", "items": { "type": "object", "properties": { "class": { "type": "string" }, "count": { "type": "integer", "minimum": 0 } } } }, "test": { "type": "array", "items": { "type": "object", "properties": { "class": { "type": "string" }, "count": { "type": "integer", "minimum": 0 } } } } } }, "auc": { "type": "object", "nullable": true, "properties": { "variant": { "type": "string", "description": "Binary ROC AUC or multiclass one-vs-rest with an unweighted macro average." }, "class_order": { "type": "array", "items": { "type": "string" } }, "positive_class": { "type": "string", "nullable": true }, "available": { "type": "boolean" }, "unavailable_reason": { "type": "string", "nullable": true } } } } }, "n_train": { "type": "integer", "nullable": true, "minimum": 1 }, "n_test": { "type": "integer", "nullable": true, "minimum": 1 }, "n_sampled_from": { "type": "integer", "nullable": true, "minimum": 1 }, "warnings": { "type": "array", "nullable": true, "items": { "type": "string" } }, "feature_importance": { "type": "array", "nullable": true, "description": "With explain=true: every feature column, most useful first. importance is the drop in the trial score (accuracy for classification, R2 for regression) on the test rows when that column is shuffled. Near zero or negative means the trial did not rely on it. Two closely related columns share their importance and can both look weak. Null without explain.", "items": { "type": "object", "properties": { "feature": { "type": "string" }, "importance": { "type": "number" } } } } } } } } } } }, "400": { "description": "- `MALFORMED_JSON`: The request body is not valid JSON.\n- `REQUEST_ABORTED`: The request ended before its body was fully received.\n- `BAD_REQUEST`: The request headers or body could not be read.\n- `UPLOAD_INVALID`: The multipart upload was rejected (for example an unexpected file field).\n- `DATA_REQUIRED`: No data: send JSON 'data' or a CSV file (field: file).\n- `TARGET_REQUIRED`: No target column was given.\n- `CSV_UNREADABLE`: The CSV file could not be parsed.\n- `TARGET_COLUMN_MISSING`: The target column is not in the data.\n- `DATASET_TOO_LARGE`: Too many rows or columns for the engine. Send fewer, or page through the data.\n- `UNKNOWN_TASK`: task must be classification or regression.\n- `TOO_MANY_COLUMNS`: More feature columns than the plan allows (limits.max_feature_columns in GET /v1/plans), target excluded.\n- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_JSON", "REQUEST_ABORTED", "BAD_REQUEST", "UPLOAD_INVALID", "DATA_REQUIRED", "TARGET_REQUIRED", "CSV_UNREADABLE", "TARGET_COLUMN_MISSING", "DATASET_TOO_LARGE", "UNKNOWN_TASK", "TOO_MANY_COLUMNS", "INVALID_INPUT" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "413": { "description": "- `PAYLOAD_TOO_LARGE`: The JSON body is too large. Send the data as a CSV file (field: file).\n- `FILE_TOO_LARGE`: The uploaded file exceeds the size limit.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "PAYLOAD_TOO_LARGE", "FILE_TOO_LARGE" ] } } } ] } } } }, "415": { "description": "- `UNSUPPORTED_CHARSET`: The body charset is not supported. Send UTF-8.\n- `UNSUPPORTED_ENCODING`: The Content-Encoding is not supported. Send the body uncompressed or gzipped.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "UNSUPPORTED_CHARSET", "UNSUPPORTED_ENCODING" ] } } } ] } } } }, "422": { "description": "- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.\n- `VALIDATION_ERROR`: The engine refused a request parameter.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INVALID_INPUT", "VALIDATION_ERROR" ] } } } ] } } } }, "500": { "description": "- `DATA_KEY_NOT_SENT`: Platform encryption defect; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_MALFORMED`: Platform encryption defect; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_MISMATCH`: The model data key could not be verified; contact support with the X-Spavik-Request-Id response header.\n- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "DATA_KEY_NOT_SENT", "DATA_KEY_MALFORMED", "DATA_KEY_MISMATCH", "INTERNAL_ERROR" ] } } } ] } } } }, "503": { "description": "- `ENGINE_UNAVAILABLE`: The prediction engine is unreachable or failed, or another engine than the one selected answered. Follow the retry policy.\n- `ENGINE_BUSY`: The prediction engine has no available capacity. Retry after Retry-After.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_UNAVAILABLE", "ENGINE_BUSY" ] } } } ] } } } }, "4XX": { "description": "- `ENGINE_ERROR`: Other refusal from the engine, relayed with its status and reason.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Archive a model (https://docs.spavik.co/api-reference/models/post-v1-models-id-archive) POST /v1/models/{id}/archive OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models/{id}/archive": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Model identifier or slug" }, { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "post": { "x-audience": "client", "tags": [ "Models" ], "summary": "Set a model aside", "description": "Nothing is destroyed: the trained artefacts are kept, the model simply stops serving and stops counting against the model quota. Restore it whenever you need it again. Predicting on an archived model returns 409 MODEL_ARCHIVED.", "responses": { "204": { "description": "Archived" }, "400": { "description": "- `MALFORMED_PATH`: A path segment is not valid percent-encoded text.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_PATH" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "404": { "description": "- `MODEL_NOT_FOUND`: No model with this identifier or slug in the workspace of the key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_NOT_FOUND" ] } } } ] } } } }, "409": { "description": "- `MODEL_ARCHIVED`: The model is archived. Restore it first.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_ARCHIVED" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Submit outcomes (https://docs.spavik.co/api-reference/models/post-v1-models-id-outcomes) POST /v1/models/{id}/outcomes OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models/{id}/outcomes": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Model identifier or slug" }, { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "post": { "x-audience": "client", "tags": [ "Models" ], "summary": "Report what actually happened", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string" }, "description": "Same workspace, method, path, key and payload prevent another execution or charge, subject to current model access. Successful responses are replayed for every data policy from a technical idempotency store (default retention 24 hours from operation creation). The data policy only controls the archiving of inference inputs and outputs; it never changes replay behavior. A different key or an expired completed key starts a new operation and may be charged again. REQUEST_IN_PROGRESS: wait Retry-After. REQUEST_INTERRUPTED: outcome uncertain; contact support. IDEMPOTENCY_RESPONSE_UNAVAILABLE: a completed response is missing; do not retry with a new key. Completed keys expire; unresolved keys remain protected. Reusing a key with different input returns 422. A key is required on the original call for safe retries." } ], "description": "Every outcome requires features and actual, on every plan and under every data policy. features must be a non-empty object. request_id is optional correlation metadata and never replaces features; inference inputs are not looked up during ingestion. Outcomes are functional model data and require no retention opt-in. Pending outcomes are kept until integration or model deletion. Optional client-supplied predictions populate observed performance and aggregate quality reports. Outcomes pile up, then join the context data of the model (POST /v1/models/{id}/refresh), and serve the predictions that follow. Each outcome carries its full set of features, optionally with the original request_id. **Send outcomes in batches**: one call is one batch, and on a model with automatic updates (auto_update) a batch triggers an integration. Sending outcomes one by one would therefore trigger one training run per outcome.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "outcomes" ], "properties": { "outcomes": { "type": "array", "minItems": 1, "maxItems": 1000, "items": { "type": "object", "properties": { "request_id": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Optional original prediction identifier for correlation; never replaces features.", "example": "req_4Nx7Bm2VtQd9Lp3Kwr" }, "features": { "type": "object", "minProperties": 1, "additionalProperties": true, "description": "Observed feature row, required for every outcome regardless of plan or data policy." }, "actual": { "oneOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" } ], "description": "Value actually observed for the target." }, "prediction": { "oneOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" } ], "description": "Optional original prediction supplied by the client for performance measurement. Requires version and engine." }, "probabilities": { "type": "object", "additionalProperties": { "type": "number", "minimum": 0, "maximum": 1 } }, "version": { "type": "integer", "minimum": 1, "description": "Model version that produced the supplied prediction." }, "engine": { "type": "string", "minLength": 1, "maxLength": 64, "description": "Exact engine that produced the supplied prediction; aliases are not accepted." } }, "required": [ "features", "actual" ], "dependentRequired": { "prediction": [ "version", "engine" ] } } } } }, "example": { "outcomes": [ { "request_id": "req_4Nx7Bm2VtQd9Lp3Kwr", "features": { "plan": "pro", "seats": 12, "tickets_90d": 3 }, "actual": 1 }, { "features": { "plan": "free", "seats": 2, "tickets_90d": 10 }, "actual": 0 } ] } } } }, "responses": { "201": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "type": "object", "properties": { "accepted": { "type": "integer" }, "pending_outcomes": { "type": "integer" }, "unresolved": { "type": "array", "deprecated": true, "maxItems": 0, "items": { "type": "string" }, "description": "Deprecated: always an empty array, kept so existing clients do not break. Do not read it. Every outcome carries its own features, so no outcome is ever left unresolved." }, "refresh_triggered": { "type": "boolean", "description": "Model on auto_update: this batch started an integration." }, "refresh_suggested": { "type": "boolean", "description": "True when no integration was triggered by this batch and pending outcomes reached the suggestion threshold (50 by default). Call POST /v1/models/{id}/refresh to fold them in. Without auto_update, the model.stale webhook is also sent each time the pending count crosses a multiple of that threshold." } } } } } }, "400": { "description": "- `MALFORMED_JSON`: The request body is not valid JSON.\n- `REQUEST_ABORTED`: The request ended before its body was fully received.\n- `BAD_REQUEST`: The request headers or body could not be read.\n- `MALFORMED_PATH`: A path segment is not valid percent-encoded text.\n- `IDEMPOTENCY_KEY_TOO_LONG`: The Idempotency-Key header is too long.\n- `VALIDATION_ERROR`: Every outcome requires a non-empty features object and an actual value. The whole batch is rejected before any outcome is stored.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_JSON", "REQUEST_ABORTED", "BAD_REQUEST", "MALFORMED_PATH", "IDEMPOTENCY_KEY_TOO_LONG", "VALIDATION_ERROR" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "404": { "description": "- `MODEL_NOT_FOUND`: No model with this identifier or slug in the workspace of the key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_NOT_FOUND" ] } } } ] } } } }, "409": { "description": "- `MODEL_ARCHIVED`: The model is archived. Restore it first.\n- `REQUEST_IN_PROGRESS`: A call with this Idempotency-Key is still running. Retry with the same key after Retry-After.\n- `REQUEST_INTERRUPTED`: A call with this Idempotency-Key was interrupted; its outcome is uncertain. Do not retry automatically.\n- `IDEMPOTENCY_RESPONSE_UNAVAILABLE`: The call completed but its response was not saved. Do not retry with a new key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_ARCHIVED", "REQUEST_IN_PROGRESS", "REQUEST_INTERRUPTED", "IDEMPOTENCY_RESPONSE_UNAVAILABLE" ] } } } ] } } } }, "413": { "description": "- `PAYLOAD_TOO_LARGE`: The JSON body is too large. Send the data as a CSV file (field: file).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "PAYLOAD_TOO_LARGE" ] } } } ] } } } }, "415": { "description": "- `UNSUPPORTED_CHARSET`: The body charset is not supported. Send UTF-8.\n- `UNSUPPORTED_ENCODING`: The Content-Encoding is not supported. Send the body uncompressed or gzipped.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "UNSUPPORTED_CHARSET", "UNSUPPORTED_ENCODING" ] } } } ] } } } }, "422": { "description": "- `IDEMPOTENCY_KEY_REUSED`: This Idempotency-Key was already used with a different payload.\n- `OUTCOME_PREDICTION_INVALID`: A supplied prediction, version or engine does not match the model.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "IDEMPOTENCY_KEY_REUSED", "OUTCOME_PREDICTION_INVALID" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.\n- `MODEL_KEY_MISSING`: The model encryption key is missing; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_UNREADABLE`: The model encryption key cannot be read; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_VERSION_UNKNOWN`: The model encryption key generation is unknown; contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR", "MODEL_KEY_MISSING", "DATA_KEY_UNREADABLE", "DATA_KEY_VERSION_UNKNOWN" ] } } } ] } } } }, "503": { "description": "- `IDEMPOTENCY_RESPONSE_UNAVAILABLE`: The response could not be saved for replay; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEK_MISSING`: Platform encryption is not configured. Retry later.\n- `DATA_KEK_INVALID`: Platform encryption is misconfigured. Retry later.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "IDEMPOTENCY_RESPONSE_UNAVAILABLE", "DATA_KEK_MISSING", "DATA_KEK_INVALID" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Refresh a model (https://docs.spavik.co/api-reference/models/post-v1-models-id-refresh) POST /v1/models/{id}/refresh OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models/{id}/refresh": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Model identifier or slug" }, { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "post": { "x-audience": "client", "tags": [ "Models" ], "summary": "Fold pending outcomes into the context data", "description": "Produces a NEW version, which serves predictions right away; the previous one stays available, so rolling back is possible. No weights change: what grows is the set of examples the model looks at.", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string" }, "description": "Same workspace, method, path, key and payload prevent another execution or charge, subject to current model access. Successful responses are replayed for every data policy from a technical idempotency store (default retention 24 hours from operation creation). The data policy only controls the archiving of inference inputs and outputs; it never changes replay behavior. A different key or an expired completed key starts a new operation and may be charged again. REQUEST_IN_PROGRESS: wait Retry-After. REQUEST_INTERRUPTED: outcome uncertain; contact support. IDEMPOTENCY_RESPONSE_UNAVAILABLE: a completed response is missing; do not retry with a new key. Completed keys expire; unresolved keys remain protected. Reusing a key with different input returns 422. A key is required on the original call for safe retries." } ], "responses": { "201": { "description": "New version", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "X-Spavik-Model-Version": { "schema": { "type": "string", "example": "presage-2" }, "description": "Exact engine used to produce this result, including on an idempotent replay. Never a latest alias. Distinct from the model data version." } }, "content": { "application/json": { "schema": { "type": "object", "properties": { "model": { "$ref": "#/components/schemas/Model" }, "version": { "$ref": "#/components/schemas/ModelVersion" }, "outcomes_applied": { "type": "integer" }, "incomplete_outcomes": { "type": "integer", "description": "Outcomes that were missing at least one column of the model: folded in with an empty value, but they teach less." }, "context_size": { "type": "integer", "nullable": true, "description": "Rows kept in the context after integration. The engine samples beyond its own limit, so this number can be lower than the sum of the rows sent." } } } } } }, "400": { "description": "- `MALFORMED_PATH`: A path segment is not valid percent-encoded text.\n- `IDEMPOTENCY_KEY_TOO_LONG`: The Idempotency-Key header is too long.\n- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.\n- `DATASET_TOO_LARGE`: Too many rows or columns for the engine. Send fewer, or page through the data.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_PATH", "IDEMPOTENCY_KEY_TOO_LONG", "INVALID_INPUT", "DATASET_TOO_LARGE" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "402": { "description": "- `ENGINE_PLAN_REQUIRED`: The engine that serves the model requires a higher plan.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_PLAN_REQUIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "404": { "description": "- `MODEL_NOT_FOUND`: No model with this identifier or slug in the workspace of the key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_NOT_FOUND" ] } } } ] } } } }, "409": { "description": "- `MODEL_ARCHIVED`: The model is archived. Restore it first.\n- `MODEL_NOT_TRAINED`: The model has no version able to serve yet.\n- `REQUEST_IN_PROGRESS`: A call with this Idempotency-Key is still running. Retry with the same key after Retry-After.\n- `REQUEST_INTERRUPTED`: A call with this Idempotency-Key was interrupted; its outcome is uncertain. Do not retry automatically.\n- `IDEMPOTENCY_RESPONSE_UNAVAILABLE`: The call completed but its response was not saved. Do not retry with a new key.\n- `NO_PENDING_OUTCOMES`: No outcome is waiting to be folded in.\n- `REFRESH_IN_PROGRESS`: A refresh of this model is already running.\n- `MODEL_NOT_REFRESHABLE`: The served version has no retained context to extend. Create a new model.\n- `ENGINE_MISMATCH`: The model family and the selected engine are incompatible.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_ARCHIVED", "MODEL_NOT_TRAINED", "REQUEST_IN_PROGRESS", "REQUEST_INTERRUPTED", "IDEMPOTENCY_RESPONSE_UNAVAILABLE", "NO_PENDING_OUTCOMES", "REFRESH_IN_PROGRESS", "MODEL_NOT_REFRESHABLE", "ENGINE_MISMATCH" ] } } } ] } } } }, "422": { "description": "- `IDEMPOTENCY_KEY_REUSED`: This Idempotency-Key was already used with a different payload.\n- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.\n- `VALIDATION_ERROR`: The engine refused a request parameter.\n- `SCHEMA_MISMATCH`: The rows do not carry the columns the model learned (see feature_columns).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "IDEMPOTENCY_KEY_REUSED", "INVALID_INPUT", "VALIDATION_ERROR", "SCHEMA_MISMATCH" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_NOT_SENT`: Platform encryption defect; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_MALFORMED`: Platform encryption defect; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_MISMATCH`: The model data key could not be verified; contact support with the X-Spavik-Request-Id response header.\n- `MODEL_KEY_MISSING`: The model encryption key is missing; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_UNREADABLE`: The model encryption key cannot be read; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_VERSION_UNKNOWN`: The model encryption key generation is unknown; contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR", "DATA_KEY_NOT_SENT", "DATA_KEY_MALFORMED", "DATA_KEY_MISMATCH", "MODEL_KEY_MISSING", "DATA_KEY_UNREADABLE", "DATA_KEY_VERSION_UNKNOWN" ] } } } ] } } } }, "502": { "description": "- `ENGINE_NO_MODEL`: The engine did not return a persisted version. Retry with the original Idempotency-Key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_NO_MODEL" ] } } } ] } } } }, "503": { "description": "- `IDEMPOTENCY_RESPONSE_UNAVAILABLE`: The response could not be saved for replay; contact support with the X-Spavik-Request-Id response header.\n- `ENGINE_UNAVAILABLE`: The prediction engine is unreachable or failed, or another engine than the one selected answered. Follow the retry policy.\n- `ENGINE_BUSY`: The prediction engine has no available capacity. Retry after Retry-After.\n- `DATA_KEK_MISSING`: Platform encryption is not configured. Retry later.\n- `DATA_KEK_INVALID`: Platform encryption is misconfigured. Retry later.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "IDEMPOTENCY_RESPONSE_UNAVAILABLE", "ENGINE_UNAVAILABLE", "ENGINE_BUSY", "DATA_KEK_MISSING", "DATA_KEK_INVALID" ] } } } ] } } } }, "4XX": { "description": "- `COLUMN_MISMATCH`: The rows do not carry the columns the model learned (see feature_columns).\n- `ENGINE_ERROR`: Other refusal from the engine, relayed with its status and reason.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "COLUMN_MISMATCH", "ENGINE_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Restore a model (https://docs.spavik.co/api-reference/models/post-v1-models-id-restore) POST /v1/models/{id}/restore OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models/{id}/restore": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Model identifier or slug" }, { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "post": { "x-audience": "client", "tags": [ "Models" ], "summary": "Put an archived model back in service", "description": "Takes a place in the model quota again, so it can be refused when the plan is full. A model deleted for good cannot be restored (409 MODEL_DELETED). If the engine has changed, predictions prepare the retained context on the selected family engine. An old version without retained context returns CONTEXT_UNAVAILABLE.", "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Model" } } } }, "400": { "description": "- `MALFORMED_PATH`: A path segment is not valid percent-encoded text.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_PATH" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "402": { "description": "- `QUOTA_MODELS_EXCEEDED`: The account already has as many active models as its plan allows. Archive another model or upgrade.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "QUOTA_MODELS_EXCEEDED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "404": { "description": "- `MODEL_NOT_FOUND`: No model with this identifier or slug in the workspace of the key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_NOT_FOUND" ] } } } ] } } } }, "409": { "description": "- `MODEL_DELETED`: The model was deleted for good; nothing to restore.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_DELETED" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Train a model (https://docs.spavik.co/api-reference/models/post-v1-models) POST /v1/models OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models": { "parameters": [ { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "post": { "x-audience": "client", "tags": [ "Models" ], "summary": "Create a model (data + target) and train it", "description": "Evaluate the table first with POST /v1/datasets/evaluate: it is free, creates nothing and returns the same validation report, so a refused table does not cost a slot in the model quota. Tables are limited to limits.max_feature_columns feature columns, target excluded (GET /v1/plans); larger tables are refused with TOO_MANY_COLUMNS. Rows beyond limits.max_context_rows are sampled down, not refused. Creating a model is training it: the data goes in the same call, as JSON ('data') or CSV (the 'file' field). The response carries the identifier to use afterwards to predict (POST /v1/models/{id}/predict), without sending the data again.", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string" }, "description": "Same workspace, method, path, key and payload prevent another execution or charge, subject to current model access. Successful responses are replayed for every data policy from a technical idempotency store (default retention 24 hours from operation creation). The data policy only controls the archiving of inference inputs and outputs; it never changes replay behavior. A different key or an expired completed key starts a new operation and may be charged again. REQUEST_IN_PROGRESS: wait Retry-After. REQUEST_INTERRUPTED: outcome uncertain; contact support. IDEMPOTENCY_RESPONSE_UNAVAILABLE: a completed response is missing; do not retry with a new key. Completed keys expire; unresolved keys remain protected. Reusing a key with different input returns 422. A key is required on the original call for safe retries." } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "data", "target" ], "properties": { "target": { "type": "string" }, "engine_family": { "type": "string", "enum": [ "flair", "presage", "prophecy" ], "example": "presage", "description": "Immutable model family. Omitted: the family marked is_default in GET /v1/engine-families. The latest active engine prepares the initial context; predictions can only use versions of this family." }, "name": { "type": "string", "description": "Model name." }, "auto_update": { "type": "boolean", "description": "Fold outcomes in without waiting for a call to /refresh. Each post to /outcomes then triggers an integration: send your outcomes in batches." }, "retain_outcomes": { "type": "boolean", "deprecated": true, "description": "Deprecated and ignored. Outcomes and refresh do not require a retention opt-in. Inference data retention is unchanged." }, "data": { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "Data rows, one object per row." } } }, "example": { "target": "churn", "engine_family": "presage", "data": [ { "plan": "pro", "seats": 12, "tickets_90d": 3, "churn": 0 }, { "plan": "free", "seats": 1, "tickets_90d": 9, "churn": 1 }, { "plan": "pro", "seats": 40, "tickets_90d": 0, "churn": 0 }, { "plan": "free", "seats": 2, "tickets_90d": 7, "churn": 1 }, { "plan": "growth", "seats": 120, "tickets_90d": 1, "churn": 0 }, { "plan": "free", "seats": 1, "tickets_90d": 12, "churn": 1 }, { "plan": "pro", "seats": 25, "tickets_90d": 2, "churn": 0 }, { "plan": "free", "seats": 3, "tickets_90d": 5, "churn": 1 }, { "plan": "growth", "seats": 80, "tickets_90d": 4, "churn": 0 }, { "plan": "pro", "seats": 8, "tickets_90d": 11, "churn": 1 }, { "plan": "growth", "seats": 200, "tickets_90d": 0, "churn": 0 }, { "plan": "free", "seats": 4, "tickets_90d": 2, "churn": 0 } ] } }, "multipart/form-data": { "schema": { "type": "object", "required": [ "file", "target" ], "properties": { "file": { "type": "string", "format": "binary", "description": "Labelled history (CSV)." }, "target": { "type": "string" }, "engine_family": { "type": "string", "enum": [ "flair", "presage", "prophecy" ], "example": "presage", "description": "Immutable model family. Omitted: the family marked is_default in GET /v1/engine-families. The latest active engine prepares the initial context; predictions can only use versions of this family." }, "name": { "type": "string", "description": "Model name." }, "auto_update": { "type": "boolean", "description": "Fold outcomes in without waiting for a call to /refresh. Each post to /outcomes then triggers an integration: send your outcomes in batches." }, "retain_outcomes": { "type": "boolean", "deprecated": true, "description": "Deprecated and ignored. Outcomes and refresh do not require a retention opt-in. Inference data retention is unchanged." } } } } } }, "responses": { "201": { "description": "Model trained", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "X-Spavik-Model-Version": { "schema": { "type": "string", "example": "presage-2" }, "description": "Exact engine used to produce this result, including on an idempotent replay. Never a latest alias. Distinct from the model data version." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TrainingResult" } } } }, "400": { "description": "- `MALFORMED_JSON`: The request body is not valid JSON.\n- `REQUEST_ABORTED`: The request ended before its body was fully received.\n- `BAD_REQUEST`: The request headers or body could not be read.\n- `UPLOAD_INVALID`: The multipart upload was rejected (for example an unexpected file field).\n- `IDEMPOTENCY_KEY_TOO_LONG`: The Idempotency-Key header is too long.\n- `DATA_REQUIRED`: No data: send JSON 'data' or a CSV file (field: file).\n- `TARGET_REQUIRED`: No target column was given.\n- `CSV_UNREADABLE`: The CSV file could not be parsed.\n- `TARGET_COLUMN_MISSING`: The target column is not in the data.\n- `DATASET_TOO_LARGE`: Too many rows or columns for the engine. Send fewer, or page through the data.\n- `UNKNOWN_TASK`: task must be classification or regression.\n- `TOO_MANY_COLUMNS`: More feature columns than the plan allows (limits.max_feature_columns in GET /v1/plans), target excluded.\n- `ENGINE_FAMILY_REQUIRED`: No engine family was given and none is marked as default.\n- `ENGINE_FAMILY_ONLY`: engine_version is not accepted at creation: choose engine_family, the version is selected at prediction time.\n- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_JSON", "REQUEST_ABORTED", "BAD_REQUEST", "UPLOAD_INVALID", "IDEMPOTENCY_KEY_TOO_LONG", "DATA_REQUIRED", "TARGET_REQUIRED", "CSV_UNREADABLE", "TARGET_COLUMN_MISSING", "DATASET_TOO_LARGE", "UNKNOWN_TASK", "TOO_MANY_COLUMNS", "ENGINE_FAMILY_REQUIRED", "ENGINE_FAMILY_ONLY", "INVALID_INPUT" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "402": { "description": "- `QUOTA_MODELS_EXCEEDED`: The account already has as many active models as its plan allows, across all workspaces. Archive a model or upgrade. Training itself is free.\n- `ENGINE_PLAN_REQUIRED`: The engine family requires a higher plan.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "QUOTA_MODELS_EXCEEDED", "ENGINE_PLAN_REQUIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "409": { "description": "- `REQUEST_IN_PROGRESS`: A call with this Idempotency-Key is still running. Retry with the same key after Retry-After.\n- `REQUEST_INTERRUPTED`: A call with this Idempotency-Key was interrupted; its outcome is uncertain. Do not retry automatically.\n- `IDEMPOTENCY_RESPONSE_UNAVAILABLE`: The call completed but its response was not saved. Do not retry with a new key.\n- `ENGINE_MISMATCH`: The model family and the selected engine are incompatible.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "REQUEST_IN_PROGRESS", "REQUEST_INTERRUPTED", "IDEMPOTENCY_RESPONSE_UNAVAILABLE", "ENGINE_MISMATCH" ] } } } ] } } } }, "413": { "description": "- `PAYLOAD_TOO_LARGE`: The JSON body is too large. Send the data as a CSV file (field: file).\n- `FILE_TOO_LARGE`: The uploaded file exceeds the size limit.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "PAYLOAD_TOO_LARGE", "FILE_TOO_LARGE" ] } } } ] } } } }, "415": { "description": "- `UNSUPPORTED_CHARSET`: The body charset is not supported. Send UTF-8.\n- `UNSUPPORTED_ENCODING`: The Content-Encoding is not supported. Send the body uncompressed or gzipped.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "UNSUPPORTED_CHARSET", "UNSUPPORTED_ENCODING" ] } } } ] } } } }, "422": { "description": "- `IDEMPOTENCY_KEY_REUSED`: This Idempotency-Key was already used with a different payload.\n- `VALIDATION_FAILED`: The data validation report blocks training; messageDetail gives the first fix.\n- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.\n- `VALIDATION_ERROR`: The engine refused a request parameter.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "IDEMPOTENCY_KEY_REUSED", "VALIDATION_FAILED", "INVALID_INPUT", "VALIDATION_ERROR" ] } } } ] } } } }, "500": { "description": "- `DATA_KEY_NOT_SENT`: Platform encryption defect; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_MALFORMED`: Platform encryption defect; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_MISMATCH`: The model data key could not be verified; contact support with the X-Spavik-Request-Id response header.\n- `MODEL_KEY_MISSING`: The model encryption key is missing; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_UNREADABLE`: The model encryption key cannot be read; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_VERSION_UNKNOWN`: The model encryption key generation is unknown; contact support with the X-Spavik-Request-Id response header.\n- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "DATA_KEY_NOT_SENT", "DATA_KEY_MALFORMED", "DATA_KEY_MISMATCH", "MODEL_KEY_MISSING", "DATA_KEY_UNREADABLE", "DATA_KEY_VERSION_UNKNOWN", "INTERNAL_ERROR" ] } } } ] } } } }, "502": { "description": "- `ENGINE_NO_MODEL`: The engine did not return a persisted model. Retry with the original Idempotency-Key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_NO_MODEL" ] } } } ] } } } }, "503": { "description": "- `IDEMPOTENCY_RESPONSE_UNAVAILABLE`: The response could not be saved for replay; contact support with the X-Spavik-Request-Id response header.\n- `ENGINE_UNAVAILABLE`: The prediction engine is unreachable or failed, or another engine than the one selected answered. Follow the retry policy.\n- `ENGINE_BUSY`: The prediction engine has no available capacity. Retry after Retry-After.\n- `DATA_KEK_MISSING`: Platform encryption is not configured. Retry later.\n- `DATA_KEK_INVALID`: Platform encryption is misconfigured. Retry later.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "IDEMPOTENCY_RESPONSE_UNAVAILABLE", "ENGINE_UNAVAILABLE", "ENGINE_BUSY", "DATA_KEK_MISSING", "DATA_KEK_INVALID" ] } } } ] } } } }, "4XX": { "description": "- `ENGINE_ERROR`: Other refusal from the engine, relayed with its status and reason.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Backtest a forecast (https://docs.spavik.co/api-reference/prediction/post-v1-forecast-backtest) POST /v1/forecast/backtest OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/forecast/backtest": { "parameters": [ { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "post": { "x-audience": "client", "tags": [ "Prediction" ], "summary": "Backtest a series before forecasting (free, creates nothing)", "description": "engine_family is optional (flair, presage or prophecy), defaulting to the family marked is_default in GET /v1/engine-families. Optionally provide a full engine_version such as presage-2 or presage-latest; otherwise the latest active version of that family is selected, with the same plan and retirement checks as forecasting. No model identifier is accepted. backtest.engine identifies the engine used. Call this before POST /v1/forecast, with the same history, horizon, point_estimate and quantiles: it is the free check of how well the series can be forecast. Creates nothing, charges nothing. Validates the series (length, regular interval, duplicate timestamps, numeric target) and, if it holds, replays windows of the history to score what the engine would have predicted against what happened. Monthly series must use month-start or month-end timestamps; other fixed monthly days, including days clamped to the end of a short month, are not supported. All series must share the same frequency and calendar anchor, evaluated in UTC.\n\nEvery HTTP 200 response includes a non-null assessment (status, metric, message). `validation` describes input validity; `assessment` compares the observed MASE with both the naive reference and the historical-median constant reference. Both must be present, valid and scored on the same evaluation points. A strict improvement over both gives `better_than_baseline`; otherwise `not_better_than_baseline`. Missing evidence or a skipped backtest gives `inconclusive`. No statistical significance or temporal predictability is asserted. No comparison is made against a fixed MASE threshold of 1.\n\nThe result applies only to the requested horizon (default 1) and point_estimate (default mean). Both point_estimate and quantiles are forwarded to the engine, as in POST /v1/forecast. Window arrays come from the engine and describe observed variation, not a confidence interval. With evaluation.protocol_version = 2, MASE is normalized per series using its initial context, then averaged with equal weight per eligible series. The assessment counts evaluation.mase_n_eval, not all backtested points; series with zero or unavailable scales are excluded from every MASE. backtest.evaluation exposes the protocol, scales, window dates, exclusions and weighting. Without protocol metadata, the older engine aggregation is preserved and no per-series weighting is implied. Do not recompute aggregates as unweighted window averages. Window indices can represent different dates across series. Counts and skipped reasons describe the available evidence. When validation fails, backtest is null and assessment.status is not_evaluated. Forward assessment unchanged; an absent or null assessment is a contract violation, not inconclusive.\n\ninclude_backtest_predictions=true adds historical evaluated observations to backtest.predictions without new inference. Future forecast values are never returned here: forecasting is billed, this route is free. Use POST /v1/forecast to get them.", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "target", "data" ], "properties": { "engine_family": { "type": "string", "enum": [ "flair", "presage", "prophecy" ], "example": "presage", "description": "Engine family for this call. Omitted: the family marked is_default in GET /v1/engine-families. No persisted model is used. engine_version must belong to this family, including when the family is omitted." }, "engine_version": { "type": "string", "pattern": "^(flair|presage|prophecy)-(latest|[1-9][0-9]*(\\.[0-9]+)?)$", "maxLength": 64, "example": "presage-2", "description": "Full engine identifier within the selected family, e.g. presage-2 or presage-latest. Omitted or -latest: latest active version of that family. A different family is refused. Responses identify the exact version used." }, "include_backtest_predictions": { "type": "boolean", "default": false, "description": "Return the historical observations already evaluated by the backtest, without additional inference or credits. On POST /v1/forecast, also set backtest=true. Does not force a skipped or disabled backtest to run. Detail is always JSON." }, "target": { "type": "string" }, "horizon": { "type": "integer", "minimum": 1, "maximum": 1000, "description": "Number of future steps (alias: prediction_length)." }, "data": { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "Data rows, one object per row." }, "timestamp": { "type": "string", "description": "Timestamp column (inferred when omitted)." }, "quantiles": { "type": "array", "items": { "type": "number" } }, "item_id": { "type": "string", "description": "Column identifying several series. Each series is forecast on its own history only." }, "point_estimate": { "type": "string", "enum": [ "mean", "median" ], "description": "Statistic served in _prediction (defaults to mean)." } } }, "example": { "engine_family": "presage", "target": "ventes", "horizon": 7, "data": [ { "date": "2026-07-01", "ventes": 100 }, { "date": "2026-07-02", "ventes": 112 }, { "date": "2026-07-03", "ventes": 118 }, { "date": "2026-07-04", "ventes": 121 }, { "date": "2026-07-05", "ventes": 130 }, { "date": "2026-07-06", "ventes": 84 }, { "date": "2026-07-07", "ventes": 72 }, { "date": "2026-07-08", "ventes": 105 }, { "date": "2026-07-09", "ventes": 117 }, { "date": "2026-07-10", "ventes": 123 }, { "date": "2026-07-11", "ventes": 126 }, { "date": "2026-07-12", "ventes": 135 }, { "date": "2026-07-13", "ventes": 89 }, { "date": "2026-07-14", "ventes": 77 }, { "date": "2026-07-15", "ventes": 110 }, { "date": "2026-07-16", "ventes": 122 }, { "date": "2026-07-17", "ventes": 128 }, { "date": "2026-07-18", "ventes": 131 }, { "date": "2026-07-19", "ventes": 140 }, { "date": "2026-07-20", "ventes": 94 }, { "date": "2026-07-21", "ventes": 82 }, { "date": "2026-07-22", "ventes": 115 }, { "date": "2026-07-23", "ventes": 127 }, { "date": "2026-07-24", "ventes": 133 }, { "date": "2026-07-25", "ventes": 136 }, { "date": "2026-07-26", "ventes": 145 }, { "date": "2026-07-27", "ventes": 99 }, { "date": "2026-07-28", "ventes": 87 } ] } }, "multipart/form-data": { "schema": { "type": "object", "required": [ "target", "file" ], "properties": { "engine_family": { "type": "string", "enum": [ "flair", "presage", "prophecy" ], "example": "presage", "description": "Engine family for this call. Omitted: the family marked is_default in GET /v1/engine-families. No persisted model is used. engine_version must belong to this family, including when the family is omitted." }, "engine_version": { "type": "string", "pattern": "^(flair|presage|prophecy)-(latest|[1-9][0-9]*(\\.[0-9]+)?)$", "maxLength": 64, "example": "presage-2", "description": "Full engine identifier within the selected family, e.g. presage-2 or presage-latest. Omitted or -latest: latest active version of that family. A different family is refused. Responses identify the exact version used." }, "include_backtest_predictions": { "type": "boolean", "default": false, "description": "Return the historical observations already evaluated by the backtest, without additional inference or credits. On POST /v1/forecast, also set backtest=true. Does not force a skipped or disabled backtest to run. Detail is always JSON." }, "target": { "type": "string" }, "horizon": { "type": "integer", "minimum": 1, "maximum": 1000 }, "prediction_length": { "type": "integer", "minimum": 1, "maximum": 1000 }, "point_estimate": { "type": "string", "enum": [ "mean", "median" ], "default": "mean" }, "quantiles": { "type": "string", "description": "Comma-separated quantiles, e.g. 0.1,0.5,0.9." }, "item_id": { "type": "string" }, "file": { "type": "string", "format": "binary" }, "timestamp": { "type": "string" } } } } } }, "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "X-Spavik-Model-Version": { "schema": { "type": "string", "example": "presage-2" }, "description": "Exact engine used to produce this result, including on an idempotent replay. Never a latest alias. Distinct from the model data version." } }, "content": { "application/json": { "schema": { "type": "object", "required": [ "assessment" ], "properties": { "target": { "type": "string" }, "timestamp": { "type": "string" }, "item_id": { "type": "string", "nullable": true }, "horizon": { "type": "integer", "minimum": 1, "maximum": 1000 }, "point_estimate": { "type": "string", "enum": [ "mean", "median" ] }, "n_rows": { "type": "integer" }, "columns": { "type": "array", "items": { "type": "string" } }, "validation": { "type": "object", "additionalProperties": true }, "assessment": { "type": "object", "additionalProperties": false, "required": [ "status", "metric", "message" ], "properties": { "status": { "type": "string", "description": "Display better_than_baseline as Better than the reference on this sample, alongside assessment.message and the evaluated observation count. A dataset without signal can beat a baseline by chance. Do not present this as a reliability or deployment approval.", "enum": [ "better_than_baseline", "not_better_than_baseline", "inconclusive", "not_evaluated" ] }, "metric": { "type": "string", "enum": [ "AUC", "MAE", "MASE" ] }, "message": { "type": "string" } }, "description": "The single API conclusion, always present and non-null in HTTP 200 responses, including when the trial cannot run. Forward status, metric and message without recomputing them from scores. An absent or null assessment is a contract violation, not an inconclusive result. This is a descriptive comparison on the evaluated observations: better means strictly better than every required reference; equality is not better. No fixed gain or sample-size threshold implies significance. Inconclusive means missing or invalid evidence, or a skipped backtest; not_evaluated means validation prevented the trial. No status establishes reliability, future performance or absence of predictive signal." }, "hint": { "type": "string", "nullable": true }, "backtest": { "type": "object", "nullable": true, "additionalProperties": true, "required": [ "engine" ], "properties": { "engine": { "type": "string", "description": "Exact engine used for this backtest.", "example": "presage-2" }, "metrics": { "type": "object", "nullable": true, "additionalProperties": { "type": "number", "nullable": true }, "description": "Scores on evaluated observations. Classification: AUC, Accuracy, AveragePrecision (binary only), LogLoss and MacroF1. Regression: MAE, RMSE, R². Forecasting adds MASE, Bias, BiasRatio and Pinball. Undefined scores are null; see metric_context.undefined_reasons. Secondary scores do not change the primary assessment.", "properties": { "AUC": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "ROC AUC, binary positive class or multiclass one-vs-rest macro average." }, "Accuracy": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1 }, "AveragePrecision": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Binary non-interpolated average precision, not trapezoidal PR-AUC. Null without positive observations." }, "LogLoss": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean negative log probability of the actual class, natural logarithm, float64 epsilon clipping. Lower is better." }, "MacroF1": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Unweighted mean of defined per-class F1 scores. Classes absent from both actual and predicted labels are excluded." }, "MAE": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean absolute error in target units." }, "RMSE": { "type": "number", "nullable": true, "minimum": 0 }, "R²": { "type": "number", "nullable": true, "description": "Null for fewer than two observations or constant actual values. Can be negative." }, "MASE": { "type": "number", "nullable": true, "minimum": 0, "description": "Compare with the scored naive baseline on the same backtest, not with a fixed threshold of 1." }, "Bias": { "type": "number", "nullable": true }, "BiasRatio": { "type": "number", "nullable": true }, "Pinball": { "type": "number", "nullable": true, "minimum": 0 } } }, "baselines": { "type": "object", "nullable": true, "description": "References indexed by metric. Read floor for the numeric score, not the whole object. Fit provenance must come from the engine; a missing fit_on does not mean train.", "additionalProperties": { "type": "object", "properties": { "floor": { "type": "number", "nullable": true }, "class_order": { "type": "array", "nullable": true, "items": { "type": "string" } }, "probabilities": { "type": "array", "nullable": true, "items": { "type": "number", "minimum": 0, "maximum": 1 } }, "undefined_reason": { "type": "string", "nullable": true }, "relative_error_reduction": { "type": "number", "nullable": true, "description": "For observed loss metrics only: 1 - model score / baseline score on identical outcomes. Fraction, possibly negative. Null for non-loss metrics, zero baseline or unavailable scores." }, "reduction_undefined_reason": { "type": "string", "nullable": true }, "label": { "type": "string" }, "higher_better": { "type": "boolean" }, "fit_on": { "type": "string", "nullable": true, "enum": [ "train", "test", "theoretical", null ], "description": "Where reference parameters were determined. MAE comparison requires train." }, "prediction": { "type": [ "number", "string", "null" ], "description": "Constant prediction fitted on training data, or null for a probability vector or forecasting reference." }, "constant_floor": { "type": "number", "nullable": true, "description": "MASE of the historical-median reference on the same evaluation points." }, "constant_label": { "type": "string", "nullable": true } } } }, "evaluation": { "type": "object", "nullable": true, "description": "Engine rolling-origin protocol. Null when not supplied. In v2, MASE excludes zero or unavailable scales and weights eligible series equally; each series weights its evaluated points equally. Missing window/MASE metadata is not inferred when the backtest is skipped.", "properties": { "metric_context": { "$ref": "#/components/schemas/MetricContext" }, "protocol_version": { "type": "integer", "example": 2 }, "interpretation": { "type": "string", "enum": [ "descriptive_only" ] }, "kind": { "type": "string", "enum": [ "rolling_origin" ] }, "requested": { "type": "boolean" }, "n_input": { "type": "integer", "minimum": 0 }, "n_missing_target": { "type": "integer", "minimum": 0 }, "sampling": { "type": "string", "enum": [ "none" ] }, "horizon": { "type": "integer", "minimum": 0 }, "max_windows": { "type": "integer", "minimum": 0 }, "min_context": { "type": "integer", "minimum": 0 }, "window_order": { "type": "string", "enum": [ "oldest_to_newest" ] }, "mase_weighting": { "type": "string", "enum": [ "equal_series_then_equal_points_within_series" ] }, "other_metrics_weighting": { "type": "string", "enum": [ "equal_evaluated_points" ] }, "mase_n_series": { "type": "integer", "minimum": 0 }, "mase_n_eval": { "type": "integer", "minimum": 0 }, "series": { "type": "array", "items": { "type": "object", "properties": { "series": { "type": "string" }, "n_points": { "type": "integer", "minimum": 0 }, "seasonal_period": { "type": "integer", "nullable": true, "minimum": 1 }, "mase_scale": { "type": "number", "nullable": true }, "n_windows": { "type": "integer", "minimum": 0 }, "n_eval": { "type": "integer", "minimum": 0 }, "scale_context_start": { "type": "string", "nullable": true, "description": "UTC timestamp; engine v2 omits the timezone suffix." }, "scale_context_end": { "type": "string", "nullable": true, "description": "UTC timestamp; engine v2 omits the timezone suffix." }, "scale_n_context": { "type": "integer", "minimum": 0 }, "backtest_excluded_reason": { "type": "string", "nullable": true }, "mase_excluded_reason": { "type": "string", "nullable": true } } } }, "windows": { "type": "array", "items": { "type": "object", "properties": { "series": { "type": "string" }, "window": { "type": "integer", "minimum": 0, "description": "Zero identifies the latest window." }, "n_context": { "type": "integer", "minimum": 0 }, "n_eval": { "type": "integer", "minimum": 0 }, "context_end": { "type": "string", "nullable": true, "description": "UTC timestamp; engine v2 omits the timezone suffix." }, "test_start": { "type": "string", "nullable": true, "description": "UTC timestamp; engine v2 omits the timezone suffix." }, "test_end": { "type": "string", "nullable": true, "description": "UTC timestamp; engine v2 omits the timezone suffix." } } } } } }, "intervals": { "$ref": "#/components/schemas/IntervalMetrics" }, "predictions": { "$ref": "#/components/schemas/BacktestPredictions" }, "by_window": { "type": "array", "nullable": true, "items": { "type": "number", "nullable": true } }, "naive_by_window": { "type": "array", "nullable": true, "items": { "type": "number", "nullable": true } }, "constant_by_window": { "type": "array", "nullable": true, "items": { "type": "number", "nullable": true } }, "n_series": { "type": "integer", "nullable": true, "minimum": 1 }, "n_windows": { "type": "integer", "nullable": true, "minimum": 1 }, "n_eval": { "type": "integer", "nullable": true, "minimum": 1, "description": "All backtested observations, including those excluded from MASE. For MASE, use evaluation.mase_n_eval in protocol v2." }, "n_context": { "type": "integer", "nullable": true, "minimum": 1 }, "seasonal_period": { "type": "integer", "nullable": true, "minimum": 1 }, "skipped": { "type": "string", "nullable": true }, "warnings": { "type": "array", "nullable": true, "items": { "type": "string" } } } } } } } } }, "400": { "description": "- `MALFORMED_JSON`: The request body is not valid JSON.\n- `REQUEST_ABORTED`: The request ended before its body was fully received.\n- `BAD_REQUEST`: The request headers or body could not be read.\n- `UPLOAD_INVALID`: The multipart upload was rejected (for example an unexpected file field).\n- `DATA_REQUIRED`: No data: send JSON 'data' or a CSV file (field: file).\n- `CSV_UNREADABLE`: The CSV file could not be parsed; messageDetail gives the reason.\n- `VALIDATION_ERROR`: A body field is invalid (target, horizon, quantiles, point_estimate...); messageDetail names it.\n- `TARGET_COLUMN_MISSING`: The target column is not in the data.\n- `TIMESTAMP_COLUMN_MISSING`: The timestamp column given is not in the data.\n- `TIMESTAMP_COLUMN_NOT_FOUND`: No timestamp column could be inferred. Name it in timestamp.\n- `TARGET_REQUIRED`: No target column was given.\n- `DATASET_TOO_LARGE`: Too many rows or columns for the engine. Send fewer, or page through the data.\n- `ENGINE_FAMILY_REQUIRED`: No engine family was given and none is marked as default.\n- `INVALID_ENGINE_VERSION`: engine_version is not a valid identifier such as presage-2 or presage-latest.\n- `ENGINE_FAMILY_MISMATCH`: engine_version belongs to another family.\n- `UNKNOWN_ENGINE_VERSION`: engine_version does not exist in the catalogue.\n- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_JSON", "REQUEST_ABORTED", "BAD_REQUEST", "UPLOAD_INVALID", "DATA_REQUIRED", "CSV_UNREADABLE", "VALIDATION_ERROR", "TARGET_COLUMN_MISSING", "TIMESTAMP_COLUMN_MISSING", "TIMESTAMP_COLUMN_NOT_FOUND", "TARGET_REQUIRED", "DATASET_TOO_LARGE", "ENGINE_FAMILY_REQUIRED", "INVALID_ENGINE_VERSION", "ENGINE_FAMILY_MISMATCH", "UNKNOWN_ENGINE_VERSION", "INVALID_INPUT" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "402": { "description": "- `ENGINE_PLAN_REQUIRED`: The selected engine requires a higher plan.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_PLAN_REQUIRED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "410": { "description": "- `ENGINE_RETIRED`: The requested engine version has been retired. Select an active version or omit engine_version.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_RETIRED" ] } } } ] } } } }, "413": { "description": "- `PAYLOAD_TOO_LARGE`: The JSON body is too large. Send the data as a CSV file (field: file).\n- `FILE_TOO_LARGE`: The uploaded file exceeds the size limit.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "PAYLOAD_TOO_LARGE", "FILE_TOO_LARGE" ] } } } ] } } } }, "415": { "description": "- `UNSUPPORTED_CHARSET`: The body charset is not supported. Send UTF-8.\n- `UNSUPPORTED_ENCODING`: The Content-Encoding is not supported. Send the body uncompressed or gzipped.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "UNSUPPORTED_CHARSET", "UNSUPPORTED_ENCODING" ] } } } ] } } } }, "422": { "description": "- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.\n- `VALIDATION_ERROR`: The engine refused a request parameter.\n- `FORECAST_FAILED`: The engine could not forecast this series: at least ten points at a regular interval are required.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INVALID_INPUT", "VALIDATION_ERROR", "FORECAST_FAILED" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } }, "503": { "description": "- `ENGINE_UNAVAILABLE`: The prediction engine is unreachable or failed, or another engine than the one selected answered. Follow the retry policy.\n- `ENGINE_BUSY`: The prediction engine has no available capacity. Retry after Retry-After.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_UNAVAILABLE", "ENGINE_BUSY" ] } } } ] } } } }, "4XX": { "description": "- `ENGINE_ERROR`: Other refusal from the engine, relayed with its status and reason.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Forecast time series (https://docs.spavik.co/api-reference/prediction/post-v1-forecast) POST /v1/forecast OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/forecast": { "parameters": [ { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "post": { "x-audience": "client", "tags": [ "Prediction" ], "summary": "Forecast a time series (zero-shot)", "description": "Backtest the same history first with POST /v1/forecast/backtest: it is free and shows how reliable a forecast at this horizon would have been. engine_family is optional (flair, presage or prophecy), defaulting to the family marked is_default in GET /v1/engine-families. Optionally provide a full engine_version such as presage-2 or presage-latest; otherwise the latest active version of that family is selected. No model identifier is accepted. The response engine identifies the exact version used. Retired versions are refused without substitution. Billed per call: the requested horizon times the credits of the serving engine (1 on the default engine), whatever the number of series or historical rows. Providing 25 historical days and requesting 7 future days costs 7 credits on an engine priced at 1, or 14 credits on an engine priced at 2. All series in the call are included. The call is refused (402 CREDITS_EXHAUSTED) before computation when this cost exceeds the available credits. The engine price is fixed for the duration of the call.\n\nSeveral series (item_id) are forecast independently, each from its own history. One series does not learn from another. Historical input rows and quantiles do not multiply the charge. With backtest=true, include_backtest_predictions=true returns the historical evaluated observations in backtest.predictions, without additional inference or credits.", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string" }, "description": "Same workspace, method, path, key and payload prevent another execution or charge, subject to current model access. Successful responses are replayed for every data policy from a technical idempotency store (default retention 24 hours from operation creation). The data policy only controls the archiving of inference inputs and outputs; it never changes replay behavior. A different key or an expired completed key starts a new operation and may be charged again. REQUEST_IN_PROGRESS: wait Retry-After. REQUEST_INTERRUPTED: outcome uncertain; contact support. IDEMPOTENCY_RESPONSE_UNAVAILABLE: a completed response is missing; do not retry with a new key. Completed keys expire; unresolved keys remain protected. Reusing a key with different input returns 422. A key is required on the original call for safe retries." } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "target", "data" ], "properties": { "engine_family": { "type": "string", "enum": [ "flair", "presage", "prophecy" ], "example": "presage", "description": "Engine family for this call. Omitted: the family marked is_default in GET /v1/engine-families. No persisted model is used. engine_version must belong to this family, including when the family is omitted." }, "engine_version": { "type": "string", "pattern": "^(flair|presage|prophecy)-(latest|[1-9][0-9]*(\\.[0-9]+)?)$", "maxLength": 64, "example": "presage-2", "description": "Full engine identifier within the selected family, e.g. presage-2 or presage-latest. Omitted or -latest: latest active version of that family. A different family is refused. Responses identify the exact version used." }, "backtest": { "type": "boolean", "default": false, "description": "Evaluate historical windows on POST /v1/forecast. The dedicated backtest route always evaluates." }, "include_backtest_predictions": { "type": "boolean", "default": false, "description": "Return the historical observations already evaluated by the backtest, without additional inference or credits. On POST /v1/forecast, also set backtest=true. Does not force a skipped or disabled backtest to run. Detail is always JSON." }, "target": { "type": "string" }, "horizon": { "type": "integer", "minimum": 1, "maximum": 1000, "description": "Number of future steps (alias: prediction_length)." }, "data": { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "Data rows, one object per row." }, "timestamp": { "type": "string", "description": "Timestamp column (inferred when omitted)." }, "quantiles": { "type": "array", "items": { "type": "number" } }, "item_id": { "type": "string", "description": "Column identifying several series. Each series is forecast on its own history only." }, "point_estimate": { "type": "string", "enum": [ "mean", "median" ], "description": "Statistic served in _prediction (defaults to mean)." } } }, "example": { "engine_family": "presage", "target": "ventes", "horizon": 7, "data": [ { "date": "2026-07-01", "ventes": 100 }, { "date": "2026-07-02", "ventes": 112 }, { "date": "2026-07-03", "ventes": 118 }, { "date": "2026-07-04", "ventes": 121 }, { "date": "2026-07-05", "ventes": 130 }, { "date": "2026-07-06", "ventes": 84 }, { "date": "2026-07-07", "ventes": 72 }, { "date": "2026-07-08", "ventes": 105 }, { "date": "2026-07-09", "ventes": 117 }, { "date": "2026-07-10", "ventes": 123 }, { "date": "2026-07-11", "ventes": 126 }, { "date": "2026-07-12", "ventes": 135 }, { "date": "2026-07-13", "ventes": 89 }, { "date": "2026-07-14", "ventes": 77 }, { "date": "2026-07-15", "ventes": 110 }, { "date": "2026-07-16", "ventes": 122 }, { "date": "2026-07-17", "ventes": 128 }, { "date": "2026-07-18", "ventes": 131 }, { "date": "2026-07-19", "ventes": 140 }, { "date": "2026-07-20", "ventes": 94 }, { "date": "2026-07-21", "ventes": 82 }, { "date": "2026-07-22", "ventes": 115 }, { "date": "2026-07-23", "ventes": 127 }, { "date": "2026-07-24", "ventes": 133 }, { "date": "2026-07-25", "ventes": 136 }, { "date": "2026-07-26", "ventes": 145 }, { "date": "2026-07-27", "ventes": 99 }, { "date": "2026-07-28", "ventes": 87 } ] } }, "multipart/form-data": { "schema": { "type": "object", "required": [ "target", "file" ], "properties": { "engine_family": { "type": "string", "enum": [ "flair", "presage", "prophecy" ], "example": "presage", "description": "Engine family for this call. Omitted: the family marked is_default in GET /v1/engine-families. No persisted model is used. engine_version must belong to this family, including when the family is omitted." }, "engine_version": { "type": "string", "pattern": "^(flair|presage|prophecy)-(latest|[1-9][0-9]*(\\.[0-9]+)?)$", "maxLength": 64, "example": "presage-2", "description": "Full engine identifier within the selected family, e.g. presage-2 or presage-latest. Omitted or -latest: latest active version of that family. A different family is refused. Responses identify the exact version used." }, "backtest": { "type": "boolean", "default": false }, "include_backtest_predictions": { "type": "boolean", "default": false, "description": "Return the historical observations already evaluated by the backtest, without additional inference or credits. On POST /v1/forecast, also set backtest=true. Does not force a skipped or disabled backtest to run. Detail is always JSON." }, "target": { "type": "string" }, "horizon": { "type": "integer", "minimum": 1, "maximum": 1000 }, "prediction_length": { "type": "integer", "minimum": 1, "maximum": 1000 }, "point_estimate": { "type": "string", "enum": [ "mean", "median" ], "default": "mean" }, "quantiles": { "type": "string", "description": "Comma-separated quantiles, e.g. 0.1,0.5,0.9." }, "item_id": { "type": "string" }, "file": { "type": "string", "format": "binary" }, "timestamp": { "type": "string" } } } } } }, "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "X-Spavik-Model-Version": { "schema": { "type": "string", "example": "presage-2" }, "description": "Exact engine used to produce this result, including on an idempotent replay. Never a latest alias. Distinct from the model data version." }, "X-Spavik-Predictions-Remaining": { "schema": { "type": "integer" }, "description": "Predictions still available after this call: monthly allowance left plus balance. An absent header means the balance is unknown, not necessarily unlimited (for example on a replay). GET /v1/usage explicitly reports unlimited_usage and predictions_remaining." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ForecastResult" } } } }, "400": { "description": "- `MALFORMED_JSON`: The request body is not valid JSON.\n- `REQUEST_ABORTED`: The request ended before its body was fully received.\n- `BAD_REQUEST`: The request headers or body could not be read.\n- `UPLOAD_INVALID`: The multipart upload was rejected (for example an unexpected file field).\n- `IDEMPOTENCY_KEY_TOO_LONG`: The Idempotency-Key header is too long.\n- `DATA_REQUIRED`: No data: send JSON 'data' or a CSV file (field: file).\n- `CSV_UNREADABLE`: The CSV file could not be parsed; messageDetail gives the reason.\n- `VALIDATION_ERROR`: A body field is invalid (target, horizon, quantiles, point_estimate...); messageDetail names it.\n- `TARGET_COLUMN_MISSING`: The target column is not in the data.\n- `TIMESTAMP_COLUMN_MISSING`: The timestamp column given is not in the data.\n- `TIMESTAMP_COLUMN_NOT_FOUND`: No timestamp column could be inferred. Name it in timestamp.\n- `HORIZON_REQUIRED`: No horizon was given.\n- `ENGINE_FAMILY_REQUIRED`: No engine family was given and none is marked as default.\n- `INVALID_ENGINE_VERSION`: engine_version is not a valid identifier such as presage-2 or presage-latest.\n- `ENGINE_FAMILY_MISMATCH`: engine_version belongs to another family.\n- `UNKNOWN_ENGINE_VERSION`: engine_version does not exist in the catalogue.\n- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.\n- `DATASET_TOO_LARGE`: Too many rows or columns for the engine. Send fewer, or page through the data.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_JSON", "REQUEST_ABORTED", "BAD_REQUEST", "UPLOAD_INVALID", "IDEMPOTENCY_KEY_TOO_LONG", "DATA_REQUIRED", "CSV_UNREADABLE", "VALIDATION_ERROR", "TARGET_COLUMN_MISSING", "TIMESTAMP_COLUMN_MISSING", "TIMESTAMP_COLUMN_NOT_FOUND", "HORIZON_REQUIRED", "ENGINE_FAMILY_REQUIRED", "INVALID_ENGINE_VERSION", "ENGINE_FAMILY_MISMATCH", "UNKNOWN_ENGINE_VERSION", "INVALID_INPUT", "DATASET_TOO_LARGE" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "402": { "description": "- `ENGINE_PLAN_REQUIRED`: The selected engine requires a higher plan.\n- `CREDITS_EXHAUSTED`: The monthly allowance and the balance cannot cover this call. Buy credits or change plan.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_PLAN_REQUIRED", "CREDITS_EXHAUSTED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "409": { "description": "- `REQUEST_IN_PROGRESS`: A call with this Idempotency-Key is still running. Retry with the same key after Retry-After.\n- `REQUEST_INTERRUPTED`: A call with this Idempotency-Key was interrupted; its outcome is uncertain. Do not retry automatically.\n- `IDEMPOTENCY_RESPONSE_UNAVAILABLE`: The call completed but its response was not saved. Do not retry with a new key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "REQUEST_IN_PROGRESS", "REQUEST_INTERRUPTED", "IDEMPOTENCY_RESPONSE_UNAVAILABLE" ] } } } ] } } } }, "410": { "description": "- `ENGINE_RETIRED`: The requested engine version has been retired. Select an active version or omit engine_version.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_RETIRED" ] } } } ] } } } }, "413": { "description": "- `PAYLOAD_TOO_LARGE`: The JSON body is too large. Send the data as a CSV file (field: file).\n- `FILE_TOO_LARGE`: The uploaded file exceeds the size limit.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "PAYLOAD_TOO_LARGE", "FILE_TOO_LARGE" ] } } } ] } } } }, "415": { "description": "- `UNSUPPORTED_CHARSET`: The body charset is not supported. Send UTF-8.\n- `UNSUPPORTED_ENCODING`: The Content-Encoding is not supported. Send the body uncompressed or gzipped.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "UNSUPPORTED_CHARSET", "UNSUPPORTED_ENCODING" ] } } } ] } } } }, "422": { "description": "- `IDEMPOTENCY_KEY_REUSED`: This Idempotency-Key was already used with a different payload.\n- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.\n- `VALIDATION_ERROR`: The engine refused a request parameter.\n- `FORECAST_FAILED`: The engine could not forecast this series: at least ten points at a regular interval are required.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "IDEMPOTENCY_KEY_REUSED", "INVALID_INPUT", "VALIDATION_ERROR", "FORECAST_FAILED" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } }, "503": { "description": "- `IDEMPOTENCY_RESPONSE_UNAVAILABLE`: The response could not be saved for replay; contact support with the X-Spavik-Request-Id response header.\n- `ENGINE_CATALOGUE_MISMATCH`: The engine price is unavailable; no credits were charged. Contact support.\n- `ENGINE_UNAVAILABLE`: The prediction engine is unreachable or failed, or another engine than the one selected answered. Follow the retry policy.\n- `USAGE_ACCOUNTING_UNAVAILABLE`: Credits could not be debited; nothing was charged. Retry with the original Idempotency-Key.\n- `ENGINE_BUSY`: The prediction engine has no available capacity. Retry after Retry-After.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "IDEMPOTENCY_RESPONSE_UNAVAILABLE", "ENGINE_CATALOGUE_MISMATCH", "ENGINE_UNAVAILABLE", "USAGE_ACCOUNTING_UNAVAILABLE", "ENGINE_BUSY" ] } } } ] } } } }, "4XX": { "description": "- `ENGINE_ERROR`: Other refusal from the engine, relayed with its status and reason.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Make predictions (https://docs.spavik.co/api-reference/prediction/post-v1-models-id-predict) POST /v1/models/{id}/predict OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/models/{id}/predict": { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Model identifier or slug" }, { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "post": { "x-audience": "client", "tags": [ "Prediction" ], "summary": "Score rows with a trained model", "description": "The model family is fixed at creation. engine_version accepts a full identifier such as presage-2 or presage-latest. Omission and -latest select the latest active version within the model family. Engines from another family are refused before computation or billing. Save results and their request_id values in your application if you need them later: no prediction-history endpoint is available on any plan. The request log contains technical metadata, not prediction values.", "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string" }, "description": "Same workspace, method, path, key and payload prevent another execution or charge, subject to current model access. Successful responses are replayed for every data policy from a technical idempotency store (default retention 24 hours from operation creation). The data policy only controls the archiving of inference inputs and outputs; it never changes replay behavior. A different key or an expired completed key starts a new operation and may be charged again. REQUEST_IN_PROGRESS: wait Retry-After. REQUEST_INTERRUPTED: outcome uncertain; contact support. IDEMPOTENCY_RESPONSE_UNAVAILABLE: a completed response is missing; do not retry with a new key. Completed keys expire; unresolved keys remain protected. Reusing a key with different input returns 422. A key is required on the original call for safe retries." } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "rows": { "type": "array", "items": { "type": "object", "additionalProperties": true }, "description": "Data rows, one object per row." }, "engine_version": { "type": "string", "pattern": "^(flair|presage|prophecy)-(latest|[1-9][0-9]*(\\.[0-9]+)?)$", "maxLength": 64, "example": "presage-2", "description": "Full engine identifier within the selected family, e.g. presage-2 or presage-latest. Omitted or -latest: latest active version of that family. A different family is refused. Responses identify the exact version used." } }, "required": [ "rows" ] }, "example": { "rows": [ { "plan": "free", "seats": 2, "tickets_90d": 7 } ] } }, "multipart/form-data": { "schema": { "type": "object", "properties": { "file": { "type": "string", "format": "binary" }, "engine_version": { "type": "string", "pattern": "^(flair|presage|prophecy)-(latest|[1-9][0-9]*(\\.[0-9]+)?)$", "maxLength": 64, "example": "presage-2", "description": "Full engine identifier within the selected family, e.g. presage-2 or presage-latest. Omitted or -latest: latest active version of that family. A different family is refused. Responses identify the exact version used." } } } } } }, "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "X-Spavik-Model-Version": { "schema": { "type": "string", "example": "presage-2" }, "description": "Exact engine used to produce this result, including on an idempotent replay. Never a latest alias. Distinct from the model data version." }, "X-Spavik-Predictions-Remaining": { "schema": { "type": "integer" }, "description": "Predictions still available after this call: monthly allowance left plus balance. An absent header means the balance is unknown, not necessarily unlimited (for example on a replay). GET /v1/usage explicitly reports unlimited_usage and predictions_remaining." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScoringResult" }, "example": { "model_id": "mdl_7Kq2Vn5RtWz3Bx8Ldm", "version": 3, "engine": "presage-2", "n_scored": 1, "took_ms": 412, "predictions": [ { "request_id": "req_4Nx7Bm2VtQd9Lp3Kwr", "prediction": 1, "proba_0": 0.18, "proba_1": 0.82 } ] } } } }, "400": { "description": "- `MALFORMED_JSON`: The request body is not valid JSON.\n- `REQUEST_ABORTED`: The request ended before its body was fully received.\n- `BAD_REQUEST`: The request headers or body could not be read.\n- `UPLOAD_INVALID`: The multipart upload was rejected (for example an unexpected file field).\n- `MALFORMED_PATH`: A path segment is not valid percent-encoded text.\n- `IDEMPOTENCY_KEY_TOO_LONG`: The Idempotency-Key header is too long.\n- `INPUT_REQUIRED`: No rows: send JSON 'rows' or a CSV file (field: file).\n- `CSV_UNREADABLE`: The CSV file could not be parsed; messageDetail gives the reason.\n- `NO_ROWS`: The rows array or the CSV file is empty.\n- `DATASET_TOO_LARGE`: Too many rows or columns for the engine. Send fewer, or page through the data.\n- `TOO_MANY_ROWS`: More rows than the plan allows per prediction call (limits.max_predict_rows).\n- `ENGINE_FAMILY_REQUIRED`: No engine family was given and none is marked as default.\n- `INVALID_ENGINE_VERSION`: engine_version is not a valid identifier such as presage-2 or presage-latest.\n- `ENGINE_FAMILY_MISMATCH`: engine_version belongs to another family.\n- `UNKNOWN_ENGINE_VERSION`: engine_version does not exist in the catalogue.\n- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MALFORMED_JSON", "REQUEST_ABORTED", "BAD_REQUEST", "UPLOAD_INVALID", "MALFORMED_PATH", "IDEMPOTENCY_KEY_TOO_LONG", "INPUT_REQUIRED", "CSV_UNREADABLE", "NO_ROWS", "DATASET_TOO_LARGE", "TOO_MANY_ROWS", "ENGINE_FAMILY_REQUIRED", "INVALID_ENGINE_VERSION", "ENGINE_FAMILY_MISMATCH", "UNKNOWN_ENGINE_VERSION", "INVALID_INPUT" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "402": { "description": "- `ENGINE_PLAN_REQUIRED`: The selected engine requires a higher plan.\n- `CREDITS_EXHAUSTED`: The monthly allowance and the balance cannot cover this call. Buy credits or change plan.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_PLAN_REQUIRED", "CREDITS_EXHAUSTED" ] } } } ] } } } }, "403": { "description": "- `ADMIN_KEY_CANNOT_PREDICT`: An admin key (sk-spv-admin-...) cannot use models or predictions. Send an integration key (sk-spv-api-...).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ADMIN_KEY_CANNOT_PREDICT" ] } } } ] } } } }, "404": { "description": "- `MODEL_NOT_FOUND`: No model with this identifier or slug in the workspace of the key.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_NOT_FOUND" ] } } } ] } } } }, "409": { "description": "- `MODEL_ARCHIVED`: The model is archived. Restore it first.\n- `MODEL_NOT_TRAINED`: The model has no version able to serve yet.\n- `REQUEST_IN_PROGRESS`: A call with this Idempotency-Key is still running. Retry with the same key after Retry-After.\n- `REQUEST_INTERRUPTED`: A call with this Idempotency-Key was interrupted; its outcome is uncertain. Do not retry automatically.\n- `IDEMPOTENCY_RESPONSE_UNAVAILABLE`: The call completed but its response was not saved. Do not retry with a new key.\n- `ENGINE_MISMATCH`: The model family and the selected engine are incompatible.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "MODEL_ARCHIVED", "MODEL_NOT_TRAINED", "REQUEST_IN_PROGRESS", "REQUEST_INTERRUPTED", "IDEMPOTENCY_RESPONSE_UNAVAILABLE", "ENGINE_MISMATCH" ] } } } ] } } } }, "410": { "description": "- `ENGINE_RETIRED`: The requested engine version has been retired. Select an active version or omit engine_version.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "ENGINE_RETIRED" ] } } } ] } } } }, "413": { "description": "- `PAYLOAD_TOO_LARGE`: The JSON body is too large. Send the data as a CSV file (field: file).\n- `FILE_TOO_LARGE`: The uploaded file exceeds the size limit.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "PAYLOAD_TOO_LARGE", "FILE_TOO_LARGE" ] } } } ] } } } }, "415": { "description": "- `UNSUPPORTED_CHARSET`: The body charset is not supported. Send UTF-8.\n- `UNSUPPORTED_ENCODING`: The Content-Encoding is not supported. Send the body uncompressed or gzipped.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "UNSUPPORTED_CHARSET", "UNSUPPORTED_ENCODING" ] } } } ] } } } }, "422": { "description": "- `IDEMPOTENCY_KEY_REUSED`: This Idempotency-Key was already used with a different payload.\n- `SCHEMA_MISMATCH`: The rows do not carry the columns the model learned (see feature_columns).\n- `CONTEXT_UNAVAILABLE`: This old version has no retained context for the selected engine. Refresh the model or create a new one.\n- `INVALID_INPUT`: The engine refused the data; messageDetail names the problem.\n- `VALIDATION_ERROR`: The engine refused a request parameter.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "IDEMPOTENCY_KEY_REUSED", "SCHEMA_MISMATCH", "CONTEXT_UNAVAILABLE", "INVALID_INPUT", "VALIDATION_ERROR" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_NOT_SENT`: Platform encryption defect; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_MALFORMED`: Platform encryption defect; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_MISMATCH`: The model data key could not be verified; contact support with the X-Spavik-Request-Id response header.\n- `MODEL_KEY_MISSING`: The model encryption key is missing; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_UNREADABLE`: The model encryption key cannot be read; contact support with the X-Spavik-Request-Id response header.\n- `DATA_KEY_VERSION_UNKNOWN`: The model encryption key generation is unknown; contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR", "DATA_KEY_NOT_SENT", "DATA_KEY_MALFORMED", "DATA_KEY_MISMATCH", "MODEL_KEY_MISSING", "DATA_KEY_UNREADABLE", "DATA_KEY_VERSION_UNKNOWN" ] } } } ] } } } }, "503": { "description": "- `IDEMPOTENCY_RESPONSE_UNAVAILABLE`: The response could not be saved for replay; contact support with the X-Spavik-Request-Id response header.\n- `ENGINE_UNAVAILABLE`: The prediction engine is unreachable or failed, or another engine than the one selected answered. Follow the retry policy.\n- `USAGE_ACCOUNTING_UNAVAILABLE`: Credits could not be debited; nothing was charged. Retry with the original Idempotency-Key.\n- `ENGINE_BUSY`: The prediction engine has no available capacity. Retry after Retry-After.\n- `DATA_KEK_MISSING`: Platform encryption is not configured. Retry later.\n- `DATA_KEK_INVALID`: Platform encryption is misconfigured. Retry later.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "IDEMPOTENCY_RESPONSE_UNAVAILABLE", "ENGINE_UNAVAILABLE", "USAGE_ACCOUNTING_UNAVAILABLE", "ENGINE_BUSY", "DATA_KEK_MISSING", "DATA_KEK_INVALID" ] } } } ] } } } }, "4XX": { "description": "- `COLUMN_MISMATCH`: The rows do not carry the columns the model learned (see feature_columns).\n- `ENGINE_ERROR`: Other refusal from the engine, relayed with its status and reason.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "COLUMN_MISMATCH", "ENGINE_ERROR" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # List plans (https://docs.spavik.co/api-reference/usage-and-billing/get-v1-plans) GET /v1/plans OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/plans": { "parameters": [ { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "get": { "x-audience": "client", "tags": [ "Usage and billing" ], "summary": "Public pricing", "security": [], "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Plan" } }, "example": [ { "id": "free", "name": "Free", "currency": "USD", "price_excludes_tax": true, "description": "For exploring Spavik and building your first predictive feature. Includes 100 predictions every month.", "prices": [], "price_per_thousand_cents": 1000, "data_policy": "training_allowed", "advanced_data_controls": false, "support_tier": "community", "signup_credits": 0, "limits": { "max_context_rows": 1000, "max_feature_columns": 30, "max_predict_rows": 1000, "max_models": 1, "included_units_per_month": 100, "unlimited_usage": false } }, { "id": "build", "name": "Build", "currency": "USD", "price_excludes_tax": true, "description": "For developers shipping predictive features in production. More usage, multiple models, and private data by default.", "prices": [ { "id": "6f0c2a8e-3b1d-4c55-9a7e-1d2b3c4d5e01", "billing_interval": "month", "amount_cents": 6900, "currency": "USD", "price_excludes_tax": true } ], "price_per_thousand_cents": 690, "data_policy": "no_training", "advanced_data_controls": false, "support_tier": "community", "signup_credits": 0, "limits": { "max_context_rows": 2000, "max_feature_columns": 30, "max_predict_rows": 1000, "max_models": 3, "included_units_per_month": 10000, "unlimited_usage": false } }, { "id": "pro", "name": "Pro", "currency": "USD", "price_excludes_tax": true, "description": "For products that rely on prediction every day. Higher limits, advanced production capabilities, and Zero Data Retention.", "prices": [ { "id": "6f0c2a8e-3b1d-4c55-9a7e-1d2b3c4d5e02", "billing_interval": "month", "amount_cents": 17900, "currency": "USD", "price_excludes_tax": true } ], "price_per_thousand_cents": 597, "data_policy": "zero_data_retention", "advanced_data_controls": false, "support_tier": "email", "signup_credits": 0, "limits": { "max_context_rows": 2000, "max_feature_columns": 30, "max_predict_rows": 1000, "max_models": 10, "included_units_per_month": 30000, "unlimited_usage": false } }, { "id": "growth", "name": "Growth", "currency": "USD", "price_excludes_tax": true, "description": "For teams scaling prediction across multiple products and workloads. High-volume usage, more models, advanced controls, and Zero Data Retention.", "prices": [ { "id": "6f0c2a8e-3b1d-4c55-9a7e-1d2b3c4d5e03", "billing_interval": "month", "amount_cents": 44900, "currency": "USD", "price_excludes_tax": true } ], "price_per_thousand_cents": 449, "data_policy": "zero_data_retention", "advanced_data_controls": true, "support_tier": "priority", "signup_credits": 0, "limits": { "max_context_rows": 2000, "max_feature_columns": 30, "max_predict_rows": 1000, "max_models": 25, "included_units_per_month": 100000, "unlimited_usage": false } }, { "id": "enterprise", "name": "Enterprise", "currency": "USD", "price_excludes_tax": true, "description": "For organizations with custom scale, security, governance, and support requirements.", "prices": [], "price_per_thousand_cents": null, "data_policy": "custom", "advanced_data_controls": null, "support_tier": "csm", "signup_credits": 0, "limits": { "max_context_rows": null, "max_feature_columns": null, "max_predict_rows": 1000, "max_models": null, "included_units_per_month": null, "unlimited_usage": false } } ] } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } } } } } } } ``` # List API requests (https://docs.spavik.co/api-reference/usage-and-billing/get-v1-requests) GET /v1/requests OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/requests": { "parameters": [ { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "get": { "x-audience": "client", "tags": [ "Usage and billing" ], "summary": "Request log (successful and failed calls)", "description": "Calls made by the active workspace, most recent first. `status=error` keeps only the failures: enough to look up a rejection that happened in production instead of reproducing it. Default window: 7 days (30 at most). Includes the billed account, authenticated actor, resolved IP, User-Agent and optional caller name and version (SDK, MCP server or direct application). Absent metadata and historical fields are null. Only calls with a resolved workspace identity enter this journal; administration, internal calls, provider webhooks and journal reads are excluded.", "parameters": [ { "name": "from", "in": "query", "schema": { "type": "string", "format": "date" } }, { "name": "to", "in": "query", "schema": { "type": "string", "format": "date" } }, { "name": "status", "in": "query", "schema": { "type": "string", "enum": [ "all", "error" ] } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 }, "description": "Calls per page." }, { "name": "cursor", "in": "query", "schema": { "type": "string", "maxLength": 512 }, "description": "next_cursor from the preceding page. Send the same from, to and status: the cursor only marks where the previous page stopped." } ], "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RequestLog" } } } }, "400": { "description": "- `VALIDATION_ERROR`: A query parameter is invalid; messageDetail names it.\n- `INVALID_DATE`: from or to is not an ISO 8601 date.\n- `INVALID_RANGE`: from is after to.\n- `RANGE_TOO_WIDE`: The window exceeds the maximum number of days.\n- `INVALID_CURSOR`: The cursor is not one returned by this route.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "VALIDATION_ERROR", "INVALID_DATE", "INVALID_RANGE", "RANGE_TOO_WIDE", "INVALID_CURSOR" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } }, "503": { "description": "- `REQUEST_LOG_UNAVAILABLE`: The request log is temporarily unreadable. Retry this GET after Retry-After.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "REQUEST_LOG_UNAVAILABLE" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Get account usage (https://docs.spavik.co/api-reference/usage-and-billing/get-v1-usage) GET /v1/usage OpenAPI contract: https://docs.spavik.co/api-reference/openapi.json ```json { "openapi": "3.1.0", "info": { "title": "Spavik API", "version": "1.0.0", "description": "Prediction and forecasting for developers. Creating a model is training it (POST /v1/models, data + target); you then predict by its id (POST /v1/models/{id}/predict). Forecasting (POST /v1/forecast) creates no persistent model. Model predictions cost the engine price per scored row. Forecasting costs the engine price per requested horizon step per call (1 credit on the default engine), with all series included; training costs nothing. Send your API key in X-Spavik-Api-Key." }, "servers": [ { "url": "https://api.spavik.co" } ], "security": [ { "apiKey": [] } ], "paths": { "/v1/usage": { "parameters": [ { "name": "User-Agent", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 1024 }, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Name", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 128 }, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, { "name": "X-Spavik-Client-Version", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 64 }, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } ], "get": { "x-audience": "client", "tags": [ "Usage and billing" ], "summary": "Account usage", "parameters": [ { "name": "from", "in": "query", "schema": { "type": "string", "format": "date" } }, { "name": "to", "in": "query", "schema": { "type": "string", "format": "date" } }, { "name": "workspace_id", "in": "query", "schema": { "type": "string", "example": "ws_3v8QpZ2mKdRt7Lc9Xw" } } ], "responses": { "200": { "description": "Success", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Usage" }, "example": { "plan": "pro", "credit_balance": 1200, "credits_low": false, "predictions_remaining": 18450, "unlimited_usage": false, "included_units_per_month": 30000, "period": { "key": "2026-10-03T00:00:00.000Z", "reset_at": "2026-11-03T00:00:00.000Z", "units_used": 12750 }, "window": { "from": "2026-10-19", "to": "2026-10-20" }, "totals": { "units": 1160, "predictions": 1265 }, "daily": [ { "date": "2026-10-19", "units": 640, "predictions": 700 }, { "date": "2026-10-20", "units": 520, "predictions": 565 } ], "by_kind": [ { "key": "predict", "label": null, "units": 1020, "predictions": 1020 }, { "key": "forecast", "label": null, "units": 140, "predictions": 245 } ], "by_workspace": [ { "key": "ws_3v8QpZ2mKdRt7Lc9Xw", "label": "Checkout app", "units": 1160, "predictions": 1265 } ], "by_api_key": [ { "key": "key_5Zt8Wn3QmXd7Bp2Kfh", "label": "Production", "units": 1160, "predictions": 1265 } ] } } } }, "400": { "description": "- `VALIDATION_ERROR`: A query parameter is invalid; messageDetail names it.\n- `INVALID_DATE`: from or to is not an ISO 8601 date.\n- `INVALID_RANGE`: from is after to.\n- `RANGE_TOO_WIDE`: The window exceeds the maximum number of days.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "VALIDATION_ERROR", "INVALID_DATE", "INVALID_RANGE", "RANGE_TOO_WIDE" ] } } } ] } } } }, "401": { "description": "- `AUTH_REQUIRED`: No API key was sent in X-Spavik-Api-Key.\n- `API_KEY_INVALID`: The API key is unknown or revoked.\n- `API_KEY_EXPIRED`: The API key is past its expiry date (for example after a rotation).", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "AUTH_REQUIRED", "API_KEY_INVALID", "API_KEY_EXPIRED" ] } } } ] } } } }, "500": { "description": "- `INTERNAL_ERROR`: Unexpected failure on the Spavik side. Retry later; if it persists, contact support with the X-Spavik-Request-Id response header.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "INTERNAL_ERROR" ] } } } ] } } } }, "503": { "description": "- `USAGE_UNAVAILABLE`: The usage log is temporarily unreadable. Retry shortly.", "headers": { "X-Spavik-Request-Id": { "schema": { "type": "string", "example": "req_..." }, "description": "HTTP request identifier for correlation with logs. Generated for this call or taken from its X-Spavik-Request-Id header; never restored from a stored idempotent result." }, "Retry-After": { "schema": { "type": "integer", "minimum": 1 }, "description": "Optional suggested delay in seconds. See x-retry-policies by error code; this header alone does not authorize replaying a mutation without its original idempotency key." } }, "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "message": { "type": "string", "enum": [ "USAGE_UNAVAILABLE" ] } } } ] } } } } }, "security": [ { "apiKey": [] } ] } } } } ``` # Authentication (https://docs.spavik.co/authentication) Connect your application or agent with a Spavik API key. Create an API key in your Spavik workspace and store it in your application's secret manager. Use that key for the REST API, Python SDK, TypeScript SDK or MCP. ## Connect with your API key Your integration uses an API key, which starts with `sk-spv-api-`. Workspace admin keys, which start with `sk-spv-admin-`, manage the workspace and cannot train or predict. Set `SPAVIK_API_KEY` as shown in [Get started](/start). Both SDKs read it and send `X-Spavik-Api-Key`. `SPAVIK_BASE_URL` is an optional SDK override; the default is `https://api.spavik.co`. The cURL examples use this variable to construct their request URLs. The examples below list the models accessible to that key. **Python** ```python from spavik import Spavik spavik = Spavik() for model in spavik.models(): print(model.id) ``` **JavaScript** ```js import { Spavik } from 'spavik'; const spavik = new Spavik(); for await (const model of spavik.models()) { console.log(model.id); } ``` **REST (cURL)** ```sh export SPAVIK_BASE_URL="${SPAVIK_BASE_URL:-https://api.spavik.co}" offset=0 while :; do page=$(curl --fail-with-body --get "$SPAVIK_BASE_URL/v1/models" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" \ --data-urlencode 'limit=50' --data-urlencode "offset=$offset") || break printf '%s' "$page" | jq -r '.data[].id' [ "$(printf '%s' "$page" | jq -r '.pagination.has_more')" = true ] || break offset=$((offset + 50)) done ``` An explicitly empty key disables the environment fallback, for example `Spavik(api_key="")` in Python or `new Spavik({ apiKey: '' })` in JavaScript: the client then sends no key rather than reading `SPAVIK_API_KEY`. The API checks permissions and quotas for the operation you request. For the [MCP server](/mcp), configure the same `X-Spavik-Api-Key` header in your client. ## Keep keys out of your code Store the secret when it is created: it is shown only once. Do not commit it, include it in browser code or print it in logs. Load it from an environment variable or secret manager at runtime. When replacing a key, update your deployed callers and verify access before retiring the previous key. Manage keys and account settings in your workspace; your integration only needs the key used for its API calls. ## When a call is refused | Status and code | Meaning | | --- | --- | | `401 AUTH_REQUIRED` | no key reached the API: check the `X-Spavik-Api-Key` header | | `401 API_KEY_INVALID` | the key is unknown, malformed or revoked | | `401 API_KEY_EXPIRED` | the key has passed its expiry date: create a new one | | `403 ADMIN_KEY_CANNOT_PREDICT` | an admin key was used for training or predictions | A permission refusal is different from insufficient credits or a model quota: use the returned error code to decide what to change. See [Error codes](/reference/errors). # Core concepts (https://docs.spavik.co/concepts) Engines, your models, their versions and the prediction lifecycle. Spavik separates the prediction technology from the model you create for your business. A model has versions that record its successive states. ## The engine An engine is the prediction technology used to build and serve your model. Several customer models can use the same engine, each with its own data and prediction target. The engine determines the supported tasks and the credits charged per prediction. Read `GET /v1/engine-families` for the available families and the configured default. At training, `engine_family` is optional; the API selects its default when omitted. A model keeps the same family throughout its lifetime. `GET /v1/engines` lists exact engine versions, capabilities, prices, plan requirements and retirement dates. Choose identifiers from that catalogue: `presage` illustrates a family, `presage-2` an exact version and `presage-latest` a family alias. Use an available identifier for your account. For predictions, `engine_version` selects an engine within the model's family. Omitting it selects the latest active version in that family. Keep the exact `engine` returned with each result. Forecast and backtest also accept an optional family and engine version. See [Get started](/start). ## Your model Your model is the reusable prediction object you create for a business task: for example, predicting customer churn from your customer data. It combines your prepared examples, the target, the feature schema and the artifacts needed by the selected engine. It has a `model_id` that you use for later predictions. The target is the column you want to predict. Spavik infers the task from it: | Target contains | Task | What `predict` returns | | --- | --- | --- | | Categories (`0`/`1`, `yes`/`no`, `A`/`B`/`C`) | `classification` | The class, plus a probability per class | | Continuous numbers (an amount, a duration) | `regression` | A numeric value | Check the returned task, especially for numeric labels. New prediction rows must carry the model's feature columns, with the same preparation as training. Keep the returned identifier and schema; you do not resend the training data with each prediction. The prepared examples retained by your model are its *context data*. They belong to the model and its versions; they are not a separate object to manage. The model also keeps the artifacts required by its engine family. Integrating observed outcomes updates this model state. ## The version Training creates the first version of your model. Integrating reported outcomes with `refresh` creates a new version under the same model identifier. The active version serves predictions; earlier versions remain in the version history. A model version records a state of your data, feature schema, diagnostics and retained examples, including the engine used to prepare it. This model version is distinct from the serving engine version. The API may serve it with a newer active engine in the same family without creating another model version. Use `engine_version` to select a serving version and keep the exact returned `engine` alongside the model `version` for each prediction. > **Note** > > **n_train and n_sampled_from** > > `n_train` is the number of examples retained. If `n_sampled_from` is larger, > the engine selected a subset of the usable input rows. Review this when > interpreting the model's results. ## Prediction and forecasting Two different operations, not to be confused. | | `predict` | `forecast` | | --- | --- | --- | | Question asked | "For this row, what value?" | "For the next 7 days, what values?" | | Needs a model | yes | no | | Keeps anything | yes, a reusable model | no, nothing | | Billing | scored rows × serving engine tariff | horizon × serving engine tariff per call, all series included | Use `predict` for a label or quantity explained by features known at prediction time. Use `forecast` to extend an observed series. A future quantity can also use regression with features available at the requested horizon; dates alone do not decide the formulation. ## Checking before you commit Both workflows have a free evaluation that creates no persistent model. The question is whether the engine finds predictive signal it can exploit in the supplied data, measured against the required references. Before training, `evaluate` returns structural validation and an engine trial. Before forecasting, `backtest` replays history against later observations. It can include diagnostic predictions with `include_backtest_predictions: true`. Both return the API's `assessment`: a comparison on the evaluated observations, not a certification of reliability. Read `assessment.status`, `assessment.metric` and `assessment.message` unchanged. The statuses are `better_than_baseline`, `not_better_than_baseline`, `inconclusive` and `not_evaluated`. See [Read an evaluation](/guides/evaluation) for their meaning, metrics and observation counts. > **Warning** > > Tabular evaluation uses a random holdout and exposes no temporal or group split > controls. It cannot establish performance on future periods or unseen entities > when those require separate splits. A forecasting backtest does not evaluate an > arbitrary regression model. Compare approaches only under matching protocols. ## The feedback loop This is what separates a frozen model from a service that improves. ![Feedback loop: predict, observe the actual outcome, report it, measure performance, refresh the model, then predict again.](/images/feedback-loop.svg) Report what actually happened with `submit_outcomes`. The performance response compares predictions with matched outcomes. Read `observed.assessment` with the task's metrics, matching baselines, evaluated counts and results by version. Scores that cannot be computed remain null with a reason. Decide whether to refresh from that evidence and your use case. A deterioration alone does not establish leakage: investigate feature availability, data drift and evaluation coverage. See [The feedback loop](/guides/feedback-loop) to set it up. ## Automatic updates At creation, `auto_update` lets incoming outcomes trigger the integration, without an explicit refresh request. Configure this when [creating your model](/guides/feedback-loop#a-reasonable-rhythm). Watch out for one trap: **one call to `submit_outcomes` is one batch**. With `auto_update`, sending outcomes one at a time triggers one integration per outcome. Batch them. ## Active, archived, deleted A model is active while it serves predictions and counts against the plan's quota. Archiving it stops both: it keeps its versions, context and reported outcomes, and restoring it puts it back in service, taking a slot again. Keep prediction inputs and results in your application; there is no prediction history endpoint. Deleting it is the only irreversible move: the artefacts leave the engine and the pending outcomes go with them. A training that fails leaves nothing behind. # Costs and credits (https://docs.spavik.co/costs) Understand prediction costs and read the available balance. ## What is billed Only predictions and forecasts consume credits. Training, evaluation, backtesting, reported outcomes, model refreshes, archiving, restoring and reads are free. | Operation | Cost | | --- | --- | | `POST /v1/models`, training | 0 credits | | `POST /v1/datasets/evaluate` | 0 credits | | `POST /v1/models/{id}/predict` | Scored rows × the model engine's credits per row | | `POST /v1/forecast` | `horizon` × the serving engine's credits, per call, all series included | | `POST /v1/forecast/backtest` | 0 credits | | `POST /v1/models/{id}/outcomes` | 0 credits | | `POST /v1/models/{id}/refresh` | 0 credits | | Reads | 0 credits | For forecasting, `horizon` is the number of future steps requested for each series. On an engine charging one credit, one series with 56 historical observations and a horizon of 7 costs 7 credits. Five series with that horizon in one call also cost **7 credits**, while producing 35 forecast values. Five separate calls at horizon 7 cost 35 credits in total. Historical input rows and returned quantiles do not multiply the charge. Use `item_id` to identify separate series (`itemId` in JavaScript). For model predictions, read `GET /v1/engines` for the credits per row of the engine serving your model. A prediction does not necessarily cost one credit. ## Read the available balance `GET /v1/usage` returns `predictions_remaining`: the remaining monthly allowance plus the credit balance, counted in credits. The monthly allowance is used first, followed by the credit balance. The Free plan includes 100 credits a month, reset on the first of each calendar month (UTC), and starts with no credit balance. Despite its name, this number counts credits, not rows: on an engine charging two credits per row, it scores half as many rows. **Python** ```python usage = spavik.usage() print(usage["predictions_remaining"]) ``` **JavaScript** ```js const usage = await spavik.usage(); console.log(usage.predictions_remaining); ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/usage" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" | jq '.predictions_remaining' ``` Billable responses can include `X-Spavik-Predictions-Remaining`, the same number after the call. The SDKs expose it as `result.predictions_remaining` in Python and `result.predictionsRemaining` in JavaScript. The header can be absent, including on a cached idempotent replay, so `None` or `null` means unknown, never unlimited. The API does not return the amount charged: the SDK's `result.cost` is `0` for a free operation and `None` or `null` for a billed one. Balance responses are snapshots, and usage reports may be cached. Other calls can consume credits between reading a balance and submitting your next request. ### When usage returns null In a successful `GET /v1/usage` response, `unlimited_usage: true` and an explicit `predictions_remaining: null` denote an account without an enforced prediction credit limit. The current API applies this behavior to Enterprise accounts. It does not remove model, input-size or other technical limits. This meaning is specific to that field and response. A missing field or header means availability is unknown. `included_units_per_month: null` does not imply unlimited usage: the Free plan also has no monthly allowance and spends its finite credit balance. ## When the balance runs out If available credits cannot cover an operation, the API returns `402 CREDITS_EXHAUSTED`. These examples make a billable prediction and handle insufficient credits. Use `model` and `rows` from your application, or set `MODEL_ID` and prepare `rows.json` for REST. Inspect the error code and human-readable detail; the current API does not provide separate numeric `balance` and `required` fields in this error. **Python** ```python from spavik import SpavikCreditsExhausted try: model.predict(rows) except SpavikCreditsExhausted as exc: print(exc.code, exc.detail, exc.request_id) ``` **JavaScript** ```js import { SpavikCreditsExhausted } from 'spavik'; try { await model.predict(rows); } catch (error) { if (!(error instanceof SpavikCreditsExhausted)) throw error; console.error(error.code, error.detail, error.requestId); } ``` **REST (cURL)** ```sh # rows.json contains the same prediction rows used in the SDK examples. jq '{rows: .}' rows.json > predict-request.json status=$(curl --silent --show-error -D response-headers.txt -o response.json \ -w '%{http_code}' "$SPAVIK_BASE_URL/v1/models/$MODEL_ID/predict" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" -H "Idempotency-Key: $(uuidgen)" \ -H 'Content-Type: application/json' --data-binary @predict-request.json) if [ "$status" = 402 ]; then jq '{code: .message, detail: .messageDetail}' response.json cat response-headers.txt fi ``` ## Retry with idempotency Send a unique `Idempotency-Key` on the original billable request and reuse it with the same endpoint and payload for retries. A stored successful response is replayed without another computation or charge while its record is retained. The default retention is 24 hours from the original request. After expiration, the same key can execute and charge again. The SDKs generate a key per call and keep it during their automatic retries. Calling `predict` or `forecast` again creates a new logical request with a new key. It is not a replay of the previous SDK call. | Response | What to do | | --- | --- | | `REQUEST_IN_PROGRESS` | Wait as directed by `Retry-After`; retain the original key and payload. | | `IDEMPOTENCY_KEY_REUSED` | The key was used with different input. Check the original request. | | `REQUEST_INTERRUPTED` | The outcome is uncertain. Stop automatic retries and inspect the request log or contact support. | | `IDEMPOTENCY_RESPONSE_UNAVAILABLE` | The operation may have completed but its response could not be saved. Contact support with `X-Spavik-Request-Id`. | Do not create a new key to force execution after an uncertain outcome. An error or lost response does not prove that no charge occurred. Idempotency is protection for retries under these conditions, not a permanent guarantee against duplicate charges. See [API conventions](/guides/conventions). # API conventions (https://docs.spavik.co/guides/conventions) Errors, retries, idempotency and pagination for API integrations. ## API environment The API is served at `https://api.spavik.co`, which both SDKs use by default. The request builder on each endpoint page calls the server declared in the OpenAPI contract, with an API key sent in `X-Spavik-Api-Key`; see [Authentication](/authentication). The engine and plan catalogues are public. Each endpoint page includes request fields, response schemas and examples. Download the [OpenAPI specification](/api-reference/openapi.json) to import the contract into your own tools. ## The error envelope Every error has the same shape. ```json { "status": "error", "message": "CREDITS_EXHAUSTED", "messageDetail": "Not enough credits." } ``` `message` carries an uppercase **machine code**, to read in your code. `messageDetail` carries a readable sentence, to show or to log. Never branch your logic on `messageDetail`: it is prose, it can change. The full list is on the [Error codes](/reference/errors) page. ## The `request_id` Every response carries an `X-Spavik-Request-Id` header. It is the only thing worth quoting if you open a ticket: it locates the exact call in the logs. Use the configured client and prediction inputs from [Get started](/start). SDK exceptions expose the response identifier. For REST, save the headers alongside the response body; `MODEL_ID` identifies your model and `rows.json` contains the array of prediction rows. These examples make a billable call. **Python** ```python from spavik import SpavikError try: result = model.predict(rows) print(result.request_id) except SpavikError as error: print(error.code, error.request_id) ``` **JavaScript** ```js import { SpavikError } from 'spavik'; try { const result = await model.predict(rows); console.log(result.requestId); } catch (error) { if (!(error instanceof SpavikError)) throw error; console.error(error.code, error.requestId); } ``` **REST (cURL)** ```sh jq '{rows: .}' rows.json > predict-request.json curl --silent --show-error --fail-with-body -D response-headers.txt \ "$SPAVIK_BASE_URL/v1/models/$MODEL_ID/predict" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" -H "Idempotency-Key: $(uuidgen)" \ -H 'Content-Type: application/json' --data-binary @predict-request.json \ -o response.json cat response-headers.txt cat response.json ``` ## Idempotency The engine mutations that support `Idempotency-Key` are `POST /v1/models`, `/predict`, `/outcomes`, `/refresh` and `POST /v1/forecast`. Archive and restore reach the same state however often they run, so they need no key. A key is any string of up to 255 characters; a UUID is a good choice. A longer key is refused with `400 IDEMPOTENCY_KEY_TOO_LONG`. A stored successful response is replayed without another computation or charge when you reuse the original endpoint, key and payload within the retention window (24 hours by default, starting with the original request). The key must be present on the original call. After a completed record expires, the same key can execute and charge again. The same behavior applies to every data policy, including Zero Data Retention. Successful responses are kept in a 24-hour technical cache for replay. Zero Data Retention only disables the archiving of the inputs and outputs of `evaluate`, `backtest`, `forecast` and `predict`; it does not change API bodies, parameters, responses, outcome feedback or model refresh. **Python** ```python # The SDK creates a key and preserves it during this call's automatic retries. result = model.predict(rows) print(result.predictions) ``` **JavaScript** ```js // The SDK creates a key and preserves it during this call's automatic retries. const result = await model.predict(rows); console.log(result.predictions); ``` **REST (cURL)** ```sh # Generate once for this logical operation. Retain for any retry of this request. IDEMPOTENCY_KEY=$(uuidgen) jq '{rows: .}' rows.json > predict-request.json # For a retry, repeat only this cURL command with the original key and file. curl --fail-with-body "$SPAVIK_BASE_URL/v1/models/$MODEL_ID/predict" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H 'Content-Type: application/json' --data-binary @predict-request.json ``` Use a unique key per **logical call** and preserve it across attempts. The SDKs do this for automatic retries; a fresh SDK call generates a new key. `IDEMPOTENCY_KEY_REUSED` rejects different input under an existing key. `REQUEST_IN_PROGRESS` means the original call is still active; honor `Retry-After`. `REQUEST_INTERRUPTED` and `IDEMPOTENCY_RESPONSE_UNAVAILABLE` mean the outcome is uncertain: stop automatic retries, keep `X-Spavik-Request-Id` and inspect the request log or contact support. Do not force execution with a new key. Unresolved records remain protected rather than expiring like stored successes. See [Costs and credits](/costs) for the billing implications. ## Pagination Lists are walked with `limit` and `offset`, and return a `pagination` object. ```json { "data": [], "pagination": { "total": 0, "limit": 50, "offset": 0, "has_more": false } } ``` | Route | Default `limit` | Maximum | | --- | --- | --- | | `GET /v1/models` | 50 | 200 | | `GET /v1/requests` | 100 | 500 | The libraries hide all of this: `models()` is an iterator that fetches pages as it goes. Its `limit` only sets the page size; the iterator still walks every page. **Python** ```python for model in spavik.models(limit=50): print(model.id) ``` **JavaScript** ```js for await (const model of spavik.models({ limit: 50 })) { console.log(model.id); } ``` **REST (cURL)** ```sh offset=0 while :; do page=$(curl --fail-with-body --get "$SPAVIK_BASE_URL/v1/models" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" \ --data-urlencode 'limit=50' --data-urlencode "offset=$offset") || break printf '%s' "$page" | jq -r '.data[].id' [ "$(printf '%s' "$page" | jq -r '.pagination.has_more')" = true ] || break offset=$((offset + 50)) done ``` ## Time windows Routes that read history accept `from` and `to` as ISO 8601 dates, or `days` for a rolling window. | Parameter | Default | Maximum | | --- | --- | --- | | `days` | 30 | 365 | `GET /v1/requests` additionally accepts `status=error`, which keeps only failures: enough to track down a rejection that happened in production without reproducing it. ## Retries Read the error code and the contract's `x-retry-policies`. `Retry-After`, when present, supplies a delay; it does not by itself make a mutation safe to replay. Retain the original `Idempotency-Key` and payload for retries. For `REQUEST_INTERRUPTED` or `IDEMPOTENCY_RESPONSE_UNAVAILABLE`, the outcome is uncertain. Do not switch to a new key or retry automatically. Inspect the request log and contact support with `X-Spavik-Request-Id`. `CREDITS_EXHAUSTED` requires credits or a plan change before retrying. See [Error codes](/reference/errors). ## Rate limits The `/v1` engine routes apply no per-key request rate today. The `rate_limit_per_second` value in `GET /v1/plans` is indicative and not enforced. Plan quotas still apply: rows per prediction, context rows, feature columns and active models. If a `429 TOO_MANY_REQUESTS` ever reaches you, wait for `Retry-After` and retry with the same `Idempotency-Key`; the SDKs already do. The SDKs provide bounded retries and preserve their idempotency keys within an operation. Starting a new SDK call creates a new logical operation; do not confuse it with replaying the original request. ## Identifiers They are opaque and prefixed. Do not parse them, do not rebuild them. | Prefix | Object | | --- | --- | | `mdl_` | model | | `ver_` | model version | | `whk_` | webhook | | `sk-spv-api-` | API key | ## Dates Use ISO 8601 timestamps and normalize time zones consistently. Evaluation protocol timestamps are UTC; engine protocol v2 can omit the timezone suffix. # Prepare your data (https://docs.spavik.co/guides/data) Structure your dataset, choose the target and check data quality before training or forecasting. What Spavik expects, and what it refuses. ## The minimum | Constraint | Value | | --- | --- | | Rows to train | 10 at least | | Distinct values in the target | 2 at least | | Feature columns | 30 at most on public plans, target excluded | | Context rows | 1,000 on Free, 2,000 on paid plans; extra rows are sampled | Keep only the fields available at the prediction moment. The column limit applies to evaluation and training alike: a wider table is refused with `400 TOO_MANY_COLUMNS` and nothing is created. Rows beyond the context limit are not refused: the engine learns from a random sample of them. Read the current values in `limits.max_feature_columns` and `limits.max_context_rows` of [`GET /v1/plans`](/guides/plans). The libraries check training row counts and target constraints locally. A file that is too short therefore costs neither a credit nor a round trip. ## Accepted formats Both SDKs accept CSV files, JSON files, arrays of row objects and raw CSV text. Python also accepts pandas DataFrames. REST accepts a multipart CSV file or a JSON body with a `data` array. Use the [sample dataset](/examples/churn.csv) to follow this example, after [configuring your client](/start). **Python** ```python model = spavik.train("churn.csv", target="churn") print(model.id) ``` **JavaScript** ```js const model = await spavik.train('churn.csv', { target: 'churn' }); console.log(model.id); ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/models" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" -H "Idempotency-Key: $(uuidgen)" \ -F 'target=churn' -F 'file=@churn.csv' \ | jq -r '.model.id' ``` The separator is detected (comma, semicolon, tab, pipe), quotes are handled, and numbers get their type back along the way: a `"12"` read from a CSV becomes an integer. Calling the API directly, you can send JSON in `data`, or a file as `multipart/form-data` on `POST /v1/models`, `POST /v1/forecast` and `POST /v1/models/{id}/predict`. The latter avoids serialising a large dataset into a JSON body. ## Check before training Use `POST /v1/datasets/evaluate`, or the SDK's `evaluate` method. The operation is free and returns structural validation, a trial and an `assessment`. **Python** ```python report = spavik.evaluate("churn.csv", target="churn") print(report["assessment"]) print(report.get("validation")) ``` **JavaScript** ```js const report = await spavik.evaluate('churn.csv', { target: 'churn' }); console.log(report.assessment); console.log(report.validation); ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/datasets/evaluate" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" \ -F 'target=churn' -F 'file=@churn.csv' \ | jq '{assessment, validation}' ``` ### Choose which columns to keep Add `explain` to the evaluation to rank every feature column. The response then carries `trial.feature_importance`, from the most useful column to the least useful one. Each `importance` is how much the trial score drops when the values of that column are shuffled. A value near zero means the trial did not rely on the column. Two closely related columns share their importance, so both can look weak while either one alone would matter. Start from the business meaning of each column, then use this ranking to decide what to drop. The ranking runs one extra prediction per column, so the call takes longer on wide tables. Training does not compute it. **Python** ```python report = spavik.evaluate("churn.csv", target="churn", explain=True) print(report["trial"]["feature_importance"]) ``` **JavaScript** ```js const report = await spavik.evaluate('churn.csv', { target: 'churn', explain: true }); console.log(report.trial.feature_importance); ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/datasets/evaluate" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" \ -F 'target=churn' -F 'explain=true' -F 'file=@churn.csv' \ | jq '.trial.feature_importance' ``` Read [the evaluation guide](/guides/evaluation) before choosing the next step. The trial splits rows randomly and does not provide time or group split controls. A high score is not proof of leakage, and an unimproved score is not proof that no signal exists. Avoid repeatedly optimizing fields against the same test score. ## Prepare for the prediction moment Define what one row represents, the observed target, when a prediction will be requested and which fields are available then. Keep matching-only identifiers outside the feature table. Exclude fields filled in after the outcome; construct joins, aggregates and lags using only information known at each historical cutoff. Do not invent missing labels. Preserve a recipe and use it for later predictions. Learn preprocessing only on training partitions, then apply it to held-out data. ## Missing values An empty cell becomes `null`, and the engine copes. A row whose **target** is empty, however, teaches nothing: if too many of your rows are in that state, the library says so rather than letting training run on almost nothing. `NaN` values from a pandas DataFrame are converted to `null` automatically. ## Column names The columns you send to `predict` must carry **the same names** as at training, minus the target. A mismatch gives `COLUMN_MISMATCH` or `SCHEMA_MISMATCH`. A model remembers them: `feature_columns` on `GET /v1/models/{id}`, `model.features` in the libraries. If you get the target name wrong, the library suggests the closest column: ``` SpavikDataError: the target column 'churn_90d' is missing from the data. Columns found: plan, seats, tickets_90d, churn. Did you mean 'churn'? ``` ## Size limits The API refuses beyond certain thresholds, with an explicit code: | Code | What happened | | --- | --- | | `TOO_MANY_COLUMNS` | more feature columns than the plan allows (30 on public plans, target excluded) | | `TOO_MANY_ROWS` | too many rows in a single call | | `DATASET_TOO_LARGE` | dataset too large | | `FILE_TOO_LARGE` | uploaded file too heavy | | `CSV_UNREADABLE` | the CSV could not be parsed | These thresholds depend on the plan and the deployment. Rather than guessing them, send and read the code: the message states the limit reached. ## For a time series `forecast` additionally needs: - a **value** column (`target`), numeric; - a **timestamp** column (`timestamp`), optional but recommended; - a **series identifier** column (`item_id`) if your file holds several series, one per store or per product for instance. Your series must have **at least as many points as the requested horizon**, and preferably several times more. Three points for a horizon of fourteen produces nothing usable, and the library refuses it before the call. Cost is the requested horizon per call, regardless of the number of series or the length of the history: five series with a horizon of 7 cost 7 credits. Sort timestamps within each series and use one observed value per timestamp. Resolve duplicates and gaps according to the data's meaning; missing is not necessarily zero. Do not fabricate history or append blank future rows. Monthly series must use month-start or month-end timestamps. Other fixed days, including dates clamped to the end of a short month, are unsupported. All series must share the same frequency and calendar anchor, evaluated in UTC. Do not silently move dates to satisfy these requirements. Before paying, `POST /v1/forecast/backtest` validates and replays history. Match the intended forecast's horizon, point estimate, quantiles and column roles. For multiple series, use long format and assess history separately for each one. The required references are the naive forecast and a historical-median constant. Read [the evaluation guide](/guides/evaluation) for MASE weighting and exclusions. # Understand evaluations (https://docs.spavik.co/guides/evaluation) Read evaluation results, compare baselines and understand the limits of each assessment. `evaluate` and `backtest` are free and create no persistent model. Their REST routes are `POST /v1/datasets/evaluate` and `POST /v1/forecast/backtest`. The MCP tools are `evaluate_data` and `backtest_forecast`. Configure your client with [Get started](/start); these examples use `churn.csv` and an observed `date,sales` series in `sales.csv`. ## Validation and assessment `validation` describes whether the input is structurally usable. `assessment` is the API's comparison of the engine with its required references. Read `assessment.status`, `assessment.metric` and `assessment.message` together. | Status | Meaning | | --- | --- | | `better_than_baseline` | Strictly better than every required reference on the evaluated observations | | `not_better_than_baseline` | Not better than every required reference, including equality | | `inconclusive` | Missing or invalid comparison evidence, or a skipped backtest | | `not_evaluated` | Input validation prevented the trial | Display a better result as **Better than the reference on this sample**, alongside the API message and evaluated observation count. An improvement can happen by chance. No status certifies reliability, statistical significance, future performance or absence of predictive signal. High scores alone do not prove leakage; review field availability and independence separately. Every successful evaluation response must include a non-null `assessment`. If it is absent, report a contract anomaly. Do not invent a status or substitute legacy `signal` or `verdict` fields. Preserve the API's conclusion unchanged. ## Tabular evaluation **Python** ```python report = spavik.evaluate("churn.csv", target="churn") print(report["assessment"]) trial = report.get("trial") if trial is not None: print(trial["metrics"], trial["baselines"]) print(trial.get("n_test"), trial.get("evaluation")) print(trial.get("classification_report")) ``` **JavaScript** ```js const report = await spavik.evaluate('churn.csv', { target: 'churn' }); console.log(report.assessment); if (report.trial != null) { console.log(report.trial.metrics, report.trial.baselines); console.log(report.trial.n_test, report.trial.evaluation); console.log(report.trial.classification_report); } ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/datasets/evaluate" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" -F 'target=churn' -F 'file=@churn.csv' \ | jq '{assessment, trial: (.trial | if . == null then null else {metrics, baselines, n_test, evaluation, classification_report} end)}' ``` Classification compares AUC; accuracy is secondary and cannot replace missing AUC. Regression compares MAE with a reference fitted on the training partition: `trial.baselines.MAE.fit_on` must be `train`. Each reference's score is in `floor`, for example `trial.baselines.AUC.floor`. The engine uses a random holdout. `trial.evaluation`, when available, describes the split, original train/test row positions, sampling, class counts and AUC definition. Row positions are zero-based before dropping missing targets and exclude the header. `trial.n_test` is the test observation count; `n_sampled_from` counts eligible rows before sampling, while `n_rows` counts input rows. > **Warning** > > A random split does not validate temporal regression or generalization to unseen > entities when those require time or group separation. There are no exposed > controls for those splits. Label the result exploratory when appropriate. > A forecasting backtest does not test an arbitrary tabular regression model. ## Metrics by task | Task | Primary metric | Additional scores | | --- | --- | --- | | Binary classification | `AUC` (ROC-AUC) | `AveragePrecision`, `LogLoss`, `Accuracy`, `MacroF1`, per-class report | | Multiclass classification | `AUC` (one-vs-rest macro) | `MacroF1`, `LogLoss`, `Accuracy`, per-class report | | Regression | `MAE` | `RMSE`, `R²` | | Forecasting | `MASE` | `MAE`, `RMSE`, `Bias`, `BiasRatio`, `Pinball` when quantiles exist | | Prediction intervals | Observed coverage against nominal coverage | Average width in target units; `Pinball` for quantile forecasts | These scores describe held-out observations, not the unknown error of each new prediction. Secondary metrics do not change the primary `assessment`. There are no universal score thresholds for a model to be "good" or "usable". For classification, `evaluation.metric_context` names the positive class and class order. `AveragePrecision` uses the non-interpolated precision-recall calculation, not trapezoidal PR-AUC. It is binary-only and is null when the positive class has no actual occurrences. `LogLoss` uses natural logarithms and clips probabilities at float64 machine epsilon. `MacroF1` averages defined per-class F1 values equally; a class absent from both actual and predicted labels is excluded from this average, but remains visible in the per-class report. A score that cannot be computed is `null`, with its reason in `evaluation.metric_context.undefined_reasons`. This includes missing or invalid probabilities, undefined AUC and R² on constant or insufficient actual values. Undefined is different from zero. Older engines may omit these additions. The baseline for probability scores uses constant class frequencies fitted on the training partition. Accuracy and Macro F1 use the training majority class. Regression uses the training median for MAE and training mean for RMSE and R². All references are scored on the same held-out rows as the model. Training a saved model stores `metrics`, `baselines`, `classification_report` and `evaluation` on its version. Compare the same metric on the same observations. MAE and RMSE retain the target's units. For example, a test MAE of 7 against a reference MAE of 10 can be presented as: > Mean absolute error: **7 units**, compared with **10** for the reference. > Observed error reduction: **30% on the test set**. This is an illustrative result, not a promised level of accuracy. `baselines[metric].relative_error_reduction`, when supplied, is the API's observed reduction: `0.3` means 30%, and a negative value means increased error. Read `reduction_undefined_reason` when it is null. If the field is absent, show the two scores without inventing a percentage. The SDKs and MCP do not calculate reductions or reconstruct the assessment. A zero reference error makes the relative reduction undefined. Do not compare AUC to majority-class accuracy, apply error-reduction percentages to AUC or R², or turn `1 - MASE` into a claimed improvement. ## Classification errors by class `trial.classification_report` complements the numeric `trial.metrics` object. It does not change the AUC-based assessment. The same initial evaluation is saved as `classification_report` on model versions; older engines and versions may return null. Each entry in `per_class` includes `class`, actual test `support`, `predicted_count`, `true_positive`, `precision`, `recall`, `f1` and `undefined_reasons`. Support is the number of actual occurrences of that class in the evaluated test rows, not the number of predictions. A class present but never predicted has precision null (`no_predicted_samples`), recall 0 and F1 0. A class absent from the test but predicted has precision 0, recall null (`no_actual_samples`) and F1 0. If both counts are zero, all three scores are null; F1 reports `no_actual_or_predicted_samples`. A defined score, including zero, has a null reason. Read the returned `decision_rule` and `class_order` with the scores. They explain how the engine chose the predicted class, including any tie-breaking rule. Scores use the labels actually predicted on the same test rows as Accuracy. Do not recompute them from probabilities with another threshold. `evaluated_count` reports the number of rows evaluated. ## Forecast backtesting **Python** ```python report = spavik.backtest("sales.csv", target="sales", timestamp="date", horizon=7) print(report["assessment"]) backtest = report.get("backtest") if backtest is not None: print(backtest["metrics"], backtest["baselines"]) print(backtest.get("n_eval"), backtest.get("evaluation")) print(backtest.get("intervals")) ``` **JavaScript** ```js const report = await spavik.backtest('sales.csv', { target: 'sales', timestamp: 'date', horizon: 7, }); console.log(report.assessment); if (report.backtest != null) { console.log(report.backtest.metrics, report.backtest.baselines); console.log(report.backtest.n_eval, report.backtest.evaluation); console.log(report.backtest.intervals); } ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/forecast/backtest" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" -F 'file=@sales.csv' \ -F 'target=sales' -F 'timestamp=date' -F 'horizon=7' \ | jq '{assessment, backtest: (.backtest | if . == null then null else {metrics, baselines, n_eval, evaluation, intervals} end)}' ``` These examples use the default `mean` point estimate and default quantiles. For custom settings, pass `pointEstimate` and `quantiles` in JavaScript, or `point_estimate` and `quantiles` in Python and REST. Backtest the same prepared history, column roles, horizon, `point_estimate` and `quantiles` that you intend to use for the forecast. The API accepts `mean` (the default) or `median`. The result applies to the evaluated horizon and point estimate; it returns no forecast values. The API compares MASE with both the naive reference and the historical-median constant reference on the same evaluation points. Use their returned labels and seasonal periods; do not compare MASE with a fixed threshold of 1. With `backtest.evaluation.protocol_version = 2`: - MASE uses a scale determined from each series' initial context. - Eligible series receive equal weight, and evaluated points receive equal weight within each series. - `evaluation.mase_n_eval` counts points included in MASE; `backtest.n_eval` counts all backtested points. - Series with zero or unavailable scales are excluded from all MASE comparisons. Report the exclusions and reasons returned in `evaluation.series`. Keep the API aggregates. Do not average the window scores yourself or infer v2 weighting when protocol metadata is missing. Window arrays show observed variation, not confidence intervals; a window index may represent different dates in different series. ## Read prediction intervals `backtest.intervals` reports `nominal` and `observed` coverage, the `lower` and `upper` quantile levels, `average_width`, `width_unit: "target"` and `evaluated_count`. A forecast requested with backtesting exposes the same object at `intervals`. It uses the widest available quantile pair and the same backtest observations for coverage and width. Fractions remain between 0 and 1: `0.82` is displayed as 82%. Width is the mean upper bound minus lower bound, in the target's units. No percentage denominator is assumed. Read coverage and width together with the observation count; 82% coverage for an 80% target alone does not establish calibration. Greater coverage alone is not better: excessively wide intervals can cover almost everything. The current contract does not guarantee a Pinball baseline or an interval-quality verdict; report those measurements without inventing a reference comparison. Invalid or crossed bounds give null observed coverage and width with an `undefined_reason`; fewer than two quantiles give `intervals: null`. ## Choose the next step Inspect validation issues, the comparison, sample size and protocol limitations. Train or forecast when the available evidence fits your decision and its cost. Compare approaches only on matching periods, horizons, metrics and information available at each historical prediction cutoff. # Model feedback loop (https://docs.spavik.co/guides/feedback-loop) Report observed outcomes, measure performance and update your model. A model can lose accuracy as data changes. Report outcomes to measure what happens and decide whether updating the model helps. ![Feedback loop: predict, observe the actual outcome, report it, measure performance, refresh the model, then predict again.](/images/feedback-loop.svg) Follow [Get started](/start) to configure `spavik` and create `model`. For REST, keep `MODEL_ID` from the training response. The examples below follow one customer prediction. Business identifiers stay outside the model features. ## 1. Keep the input and prediction in your application Every prediction carries a `request_id`. Save it together with the input row and result in your application. There is no prediction-history endpoint on any plan. Every outcome requires the original `features` and `actual`, regardless of data policy. `request_id` is optional correlation metadata. Keep the exact model version and serving engine too if you want to measure performance. **Python** ```python rows = [{"plan": "pro", "seats": 12, "tickets_90d": 3}] result = model.predict(rows) prediction_record = { "customer_id": "customer-123", "request_id": result.predictions[0]["request_id"], "features": rows[0], "result": result.predictions[0], "version": result.version, "engine": result.engine, } print(prediction_record) # Save this mapping in your application. ``` **JavaScript** ```js const rows = [{ plan: 'pro', seats: 12, tickets_90d: 3 }]; const result = await model.predict(rows); const predictionRecord = { customer_id: 'customer-123', request_id: result.predictions[0].request_id, features: rows[0], result: result.predictions[0], version: result.version, engine: result.engine, }; console.log(predictionRecord); // Save this mapping in your application. ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/models/$MODEL_ID/predict" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" -H "Idempotency-Key: $(uuidgen)" \ -H 'Content-Type: application/json' \ --data '{"rows":[{"plan":"pro","seats":12,"tickets_90d":3}]}' \ | jq '{customer_id: "customer-123", request_id: .predictions[0].request_id, features: {plan: "pro", seats: 12, tickets_90d: 3}, result: .predictions[0], version: .version, engine: .engine}' \ > prediction-record.json cat prediction-record.json ``` Keep this record next to your own business identifier. The technical request log does not contain inference inputs or prediction values. ## 2. Report what happened Report outcomes only after observing them. This example assumes the first customer's actual target value is `1`, and a second observed example has value `0`. Use your own observations and preserve the target's training vocabulary. Include the original features for every observed example so the batch works with every data policy. Add the saved prediction identifier when available. **Python** ```python outcomes = [ {"request_id": prediction_record["request_id"], "features": prediction_record["features"], "actual": 1, "prediction": prediction_record["result"]["prediction"], "version": prediction_record["version"], "engine": prediction_record["engine"], "probabilities": {key[6:]: value for key, value in prediction_record["result"].items() if key.startswith("proba_")}}, {"features": {"plan": "free", "seats": 2, "tickets_90d": 10}, "actual": 0}, ] response = model.submit_outcomes(outcomes) print(response.get("accepted"), response.get("pending_outcomes")) print(response.get("refresh_triggered"), response.get("refresh_suggested")) ``` **JavaScript** ```js const outcomes = [ { request_id: predictionRecord.request_id, features: predictionRecord.features, actual: 1, prediction: predictionRecord.result.prediction, version: predictionRecord.version, engine: predictionRecord.engine, probabilities: Object.fromEntries(Object.entries(predictionRecord.result) .filter(([key]) => key.startsWith('proba_')).map(([key, value]) => [key.slice(6), value])) }, { features: { plan: 'free', seats: 2, tickets_90d: 10 }, actual: 0 }, ]; const response = await model.submitOutcomes(outcomes); console.log(response.get('accepted'), response.get('pending_outcomes')); console.log(response.get('refresh_triggered'), response.get('refresh_suggested')); ``` **REST (cURL)** ```sh jq '{outcomes: [ {request_id: .request_id, features: .features, actual: 1, prediction: .result.prediction, version: .version, engine: .engine, probabilities: (.result | with_entries(select(.key | startswith("proba_"))) | with_entries(.key |= ltrimstr("proba_")))}, {features: {plan: "free", seats: 2, tickets_90d: 10}, actual: 0} ]}' prediction-record.json > outcomes-request.json curl --fail-with-body "$SPAVIK_BASE_URL/v1/models/$MODEL_ID/outcomes" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" -H "Idempotency-Key: $(uuidgen)" \ -H 'Content-Type: application/json' --data-binary @outcomes-request.json \ | jq '{accepted, pending_outcomes, refresh_triggered, refresh_suggested}' ``` The response reports `accepted` and `pending_outcomes`. `refresh_triggered` is true when this batch already started an automatic integration, on a model with `auto_update`. `refresh_suggested` is true when enough outcomes are pending for a refresh to be worth considering. Invalid batches are rejected before ingestion. > **Warning** > > **Batch your outcomes** > > One call is one batch. On a model with `auto_update`, each batch triggers an > integration: sending outcomes one at a time would trigger one integration per > outcome. ## 3. Measure, before integrating Performance works the same for every data policy. Send the original `prediction`, exact `version` and `engine` with each outcome to populate `observed` and aggregate `quality_reports`; classification outcomes may also include `probabilities`. Outcomes without a supplied prediction can still refresh the model, but do not contribute to performance measurements. The source is `client_reported_predictions`: these reports compare the predictions supplied by your application with its observed outcomes. The API never reads inference archives to calculate performance. Start with `observed.assessment`, then read the scores, matching baselines and evaluated counts. The same [metrics and interpretation rules](/guides/evaluation) apply. **Python** ```python report = model.performance(days=30) observed = report.get("observed") if observed is None: print("No outcomes with original predictions to compare yet") else: print(observed["assessment"]) print(observed["metrics"], observed["baselines"]) print(observed["metric_context"], observed["by_version"]) print(report.get("evaluation"), report.get("training_metrics")) print(report.get("training_evaluation"), report.get("pending_outcomes")) ``` **JavaScript** ```js const report = await model.performance(30); const observed = report.get('observed'); if (observed == null) { console.log('No outcomes with original predictions to compare yet'); } else { console.log(observed.assessment); console.log(observed.metrics, observed.baselines); console.log(observed.metric_context, observed.by_version); } console.log(report.get('evaluation'), report.get('training_metrics')); console.log(report.get('training_evaluation'), report.get('pending_outcomes')); ``` **REST (cURL)** ```sh curl --fail-with-body --get "$SPAVIK_BASE_URL/v1/models/$MODEL_ID/performance" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" --data-urlencode 'days=30' \ | jq '{observed: (.observed | if . == null then null else {assessment, metrics, baselines, metric_context, by_version} end), evaluation, training_metrics, training_evaluation, pending_outcomes}' ``` `observed: null` means no readable outcomes include their original predictions in this window. This is different from `observed.assessment.status: "inconclusive"`, which reports insufficient comparison evidence in an observed evaluation. A populated `observed` without a valid `assessment` is a contract anomaly; do not replace it with a conclusion inferred from accuracy or an old `verdict`. `observed` is null until outcomes include their original predictions. When available, it includes `metric`, `compared`, `value`, `baseline`, `better_than_baseline` and `by_version`. These existing summary fields use accuracy for classification and mean absolute error for regression. The full `observed.metrics` object follows the task's metric set, and `observed.assessment` compares AUC or MAE against the reference on these outcomes. Each `by_version` entry includes its own metrics, context, baselines and assessment. Probability metrics use the probabilities recorded with the original predictions. They require valid probabilities for every compared row and compatible class orders across versions. Missing probabilities are not inferred from predicted labels. Scores are null with reasons in `observed.metric_context.undefined_reasons`; label-based metrics remain available. When class orders differ across versions, read each version's probability scores. Baseline parameters come from each prediction version's training data and are scored again on these same outcomes. The reference is never fitted to the outcomes, and its original holdout score is not reused. Older versions without saved training parameters have null baselines with a reason. For MAE, RMSE and LogLoss, `baselines[metric].relative_error_reduction` is a fraction computed as `1 - model_score / floor`, where `floor` is `baselines[metric].floor`, the reference's score; a zero baseline or unavailable score yields null. The top-level `evaluation` reports the requested window start, comparison limit, counts fetched, readable and evaluated, and whether the limit was reached. `training_evaluation` retains the initial holdout context. All comparisons are descriptive; they do not establish future reliability. Inspect the comparison count and coverage before drawing conclusions. A decline can have several causes; it does not by itself prove that a feature leaked the target. Review changes in data, feature availability and the prediction context. Python returns `None` and JavaScript returns `null` when performance analytics are unavailable; REST exposes the API error response. For classification, `observed.classification_report` gives precision, recall and F1 by class with actual support, prediction counts and explicit reasons for undefined scores. Each entry of `observed.by_version` also carries its own `classification_report`, using that version's known classes. New labels found in outcomes are included. These counts cover only readable predictions supplied with outcomes in the requested time window and configured comparison limit. They do not describe all predictions emitted. `decision_rule.kind` is `recorded_prediction`: the report evaluates the classes originally announced, without applying a new threshold. No supplied predictions means `observed: null`; regression reports have `classification_report: null`. `training_classification_report` preserves the initial holdout evaluation for comparison. Refreshing a model retains that initial report and training metrics; it does not perform a fresh holdout evaluation. ## 4. Integrate pending outcomes `refresh` integrates pending outcomes into your model and produces a new version, which serves immediately. **Python** ```python response = model.refresh() if response is None: print("No pending outcomes to integrate") else: # version is the new ModelVersion object; its number is version["version"]. print(response.get("version")["version"], response.get("outcomes_applied")) print(response.get("incomplete_outcomes")) ``` **JavaScript** ```js const response = await model.refresh(); if (response === null) { console.log('No pending outcomes to integrate'); } else { // version is the new ModelVersion object; its number is version.version. console.log(response.get('version').version, response.get('outcomes_applied')); console.log(response.get('incomplete_outcomes')); } ``` **REST (cURL)** ```sh status=$(curl --silent --show-error -o refresh-response.json -w '%{http_code}' \ -X POST "$SPAVIK_BASE_URL/v1/models/$MODEL_ID/refresh" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" -H "Idempotency-Key: $(uuidgen)") if [ "$(jq -r '.message // empty' refresh-response.json)" = NO_PENDING_OUTCOMES ]; then echo 'No pending outcomes to integrate' elif [ "$status" -ge 400 ]; then cat refresh-response.json >&2 else jq '{version: .version.version, outcomes_applied, incomplete_outcomes}' refresh-response.json fi ``` `refresh` returns `None` in Python or `null` in JavaScript only for `NO_PENDING_OUTCOMES`. Other API failures, including a refresh already in progress or an unavailable dependency, are raised. Inspect the error code before deciding the next action. The previous version stays in the version history. Integration updates the examples and context retained by the model. `incomplete_outcomes` counts integrated outcomes missing some expected feature columns; those columns were included with empty values. If `refresh_triggered` was true, automatic integration has already started; do not request another refresh for that batch. ## A reasonable rhythm There is no universal answer, but this pattern works well: - report outcomes **continuously**, in batches, as your jobs run; - measure **once a week**, and inspect the observed metric, count and baseline; - consider integrating **once a day**, or when `refresh_suggested` turns true, after reviewing outcome quality and the observed assessment. A suggested refresh is not a promise of improved performance. To integrate outcome batches automatically, enable automatic updates when creating your model. This creates a new model; it does not modify the existing one from the examples above. **Python** ```python model = spavik.train("churn.csv", target="churn", auto_update=True) ``` **JavaScript** ```js const model = await spavik.train('churn.csv', { target: 'churn', autoUpdate: true }); ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/models" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" -H "Idempotency-Key: $(uuidgen)" \ -F 'target=churn' -F 'auto_update=true' -F 'file=@churn.csv' ``` ## Getting notified Two webhook events exist for exactly this: `model.stale` when the model would benefit from an integration, `model.refreshed` when a new version is in service. See [Webhooks](/guides/webhooks). # Plans and limits (https://docs.spavik.co/guides/plans) Read the current plan, model quota and prediction allowance from the API. ## Read current plans The pricing and limits are available without authentication. The SDKs have no plan-catalogue method, so the Python and JavaScript tabs use their standard HTTP clients for this public read. They use `SPAVIK_BASE_URL` when it is set, and production otherwise. **Python** ```python import json import os from urllib.request import urlopen base_url = os.environ.get("SPAVIK_BASE_URL", "https://api.spavik.co") with urlopen(base_url + "/v1/plans") as response: print(json.load(response)) ``` **JavaScript** ```js const baseUrl = process.env.SPAVIK_BASE_URL ?? 'https://api.spavik.co'; const response = await fetch(`${baseUrl}/v1/plans`); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json()); ``` **REST (cURL)** ```sh curl --fail-with-body "${SPAVIK_BASE_URL:-https://api.spavik.co}/v1/plans" | jq . ``` The plans are `free`, `build`, `pro`, `growth` and `enterprise`. Each one lists its price, monthly allowance and limits: active models (`max_models`), rows per prediction (`max_predict_rows`), context rows (`max_context_rows`) and feature columns (`max_feature_columns`, target excluded). Context rows are learned from a sample when a table is larger; feature columns are refused beyond the limit with `400 TOO_MANY_COLUMNS`. Read that response for current values instead of hardcoding a pricing table in your integration. The Free plan includes 100 predictions a month, reset on the first of each calendar month (UTC); paid plans reset on their subscription cycle. Check usage for the available balance. ## Active-model limits The limit applies across the account's workspaces. Archiving stops serving a model and frees its slot while keeping its artifacts. Restoring takes a slot again and can fail when the quota is full. Permanent deletion is a separate, irreversible API operation. **Python** ```python model.archive() spavik.restore(model.id) ``` **JavaScript** ```js await model.archive(); await spavik.restore(model.id); ``` **REST (cURL)** ```sh curl --fail-with-body -X POST "$SPAVIK_BASE_URL/v1/models/$MODEL_ID/archive" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" curl --fail-with-body -X POST "$SPAVIK_BASE_URL/v1/models/$MODEL_ID/restore" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" ``` `QUOTA_MODELS_EXCEEDED` means the plan has no active-model slot left. Choose a model to archive or change the plan before trying to create another model. ## Consumption A prediction consumes the engine's credits per scored row. Forecasting costs `horizon` × the engine's credits per call, with all series included: one credit per step on the default engine. Training, evaluation, backtesting and refreshing are free. Paid-plan allowance and credit balance contribute to the available predictions according to the plan. **Python** ```python usage = spavik.usage() print(usage["predictions_remaining"]) ``` **JavaScript** ```js const usage = await spavik.usage(); console.log(usage.predictions_remaining); ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/usage" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" | jq '.predictions_remaining' ``` `predictions_remaining` reports available credit units. In this usage response, an explicit `null` means there is no enforced prediction-credit limit; the current API applies this to Enterprise. An absent field does not establish availability, and `included_units_per_month: null` has a different meaning: Free has no monthly allowance but still spends its finite credits. On a plan with a monthly allowance, `credit_balance` alone is not the full available amount. See [Costs and credits](/costs) for balance snapshots and SDK values. ## Rate limiting and retries The engine routes apply no per-key request rate today; `rate_limit_per_second` in the plan catalogue is indicative. For a `429 TOO_MANY_REQUESTS`, honor `Retry-After` when provided and retain any original idempotency key and payload. Consult [API conventions](/guides/conventions) for retries where the outcome is uncertain. Model quota and credit exhaustion require a change in account state, not a tight retry loop. # Webhooks (https://docs.spavik.co/guides/webhooks) Rather than polling the API, get notified. Rather than polling the API, get notified. ## The four events | Event | When it fires | `data` | | --- | --- | --- | | `model.trained` | a version has just been trained and serves predictions | `model_id`, `version`, `target`, `task`, `n_train`, `metrics` | | `model.stale` | enough outcomes have piled up, a refresh would help | `model_id`, `pending_outcomes`, `hint` | | `model.refreshed` | a refresh produced a new version | `model_id`, `version`, `outcomes_applied` | | `credits.low` | your credit balance fell below the alert threshold, the same signal as `credits_low` in `GET /v1/usage` | `credit_balance` | Every event shares the same envelope: ```json { "id": "evt_example", "type": "model.refreshed", "created_at": "2026-10-11T12:00:00.000Z", "workspace_id": "ws_example", "data": { "model_id": "mdl_example", "version": 2, "outcomes_applied": 12 } } ``` The [event payload](/api-reference/events/event) page details each schema. ## Configure a destination A workspace admin creates the destination on the **Webhooks** page of the Spavik console, with an HTTPS URL. The signing secret is shown once, at creation: store it in your receiver's secret manager and load it at runtime. Never put it in your source code. ## Verifying the signature Every delivery is signed. Verify it before doing anything with the content: without that check, anyone who knows your URL can tell you anything. Each request carries a `Spavik-Signature` header: ```text Spavik-Signature: t=1760184000,v1=5f2b...e91c ``` - `t` is the signing time, in Unix seconds. - `v1` is the hex HMAC-SHA256 of `t`, a dot, then the raw request body, keyed with the webhook secret as a UTF-8 string. - Compare signatures in constant time, and reject a `t` more than 300 seconds away from your clock: this blocks replaying an intercepted call. - Each attempt is signed again with a fresh `t`; `event.id` and the payload stay the same. The [Python SDK](/libraries/python#webhooks) and the [TypeScript SDK](/libraries/javascript#webhooks) both provide `verify`, which does all of this and returns the decoded event. > **Warning** > > **The bytes, not the object** > > The signature covers the bytes as received. A body parsed into JSON and > re-serialised does not produce the same signature, even with exactly the same > data. Read the raw body before a JSON parser transforms it. ## Acknowledge delivery Return any 2xx response once the event is stored durably, then process it in your application. Delivery is at least once: - Each attempt waits 5 seconds for your response. - Network errors, timeouts, HTTP 408, 429 and 5xx are retried, up to 8 attempts, with exponential delays from 30 seconds capped at 1 hour. A `Retry-After` from your receiver is honored up to 24 hours. - Any other non-2xx response is final, and redirects are not followed. These are the default values. Deduplicate on `event.id` so that a second delivery of the same event does not repeat a business action. Events can arrive out of order. For a given model, compare `data.version` with the version you already hold, or read the current state with `GET /v1/models/{id}` rather than trusting the order of arrival. Disabling a webhook or changing its URL cancels its pending deliveries. # Introduction (https://docs.spavik.co/) Predict business outcomes and forecast time series with the REST API, Python, JavaScript or an AI agent. Spavik turns prepared business data into predictions. Evaluate a table before training a model, or backtest a time series before forecasting its next steps. Evaluation and training are free; predictions and forecasts consume credits. [Make your first prediction](/start) Set up an API key, evaluate your data and train a model. [Prepare and evaluate data](/guides/data) Choose the target, prevent leakage and select an honest evaluation protocol. [Python SDK](/libraries/python) Work with CSV, JSON, pandas and synchronous or asynchronous clients. [TypeScript SDK](/libraries/javascript) Use the typed client in Node.js and supported edge runtimes. [Connect an agent](/mcp) Twelve MCP tools, with costs announced before each billable call. [REST API reference](/api-reference/models/get-v1-engines) Explore requests, responses and examples from the OpenAPI contract. ## Two workflows | Goal | Workflow | Billing | | --- | --- | --- | | Predict a label or quantity from features | Evaluate → train → predict | Per scored row at the model engine's price | | Forecast future series values | Backtest → forecast | Horizon per call, all series included | For a reusable model, keep each input row and prediction, including its version and engine. Report the features and observed outcome later to update your model; include the saved prediction metadata to measure performance. See [the feedback loop](/guides/feedback-loop). # TypeScript SDK (https://docs.spavik.co/libraries/javascript) Use the TypeScript SDK in JavaScript or TypeScript for evaluation, models, forecasting and outcomes. ```sh npm install spavik ``` ```js import { Spavik } from 'spavik'; const spavik = new Spavik(); // reads SPAVIK_API_KEY const model = await spavik.train('churn.csv', { target: 'churn' }); console.log(await model.predict({ plan: 'pro', seats: 12, tickets_90d: 3 })); ``` No production dependency. Node 22 or later, and edge runtimes that provide `fetch` and WebCrypto. ESM and CommonJS. ## The operations | Call | Cost | | --- | --- | | `spavik.evaluate(data, { target })` | free | | `spavik.train(data, { target })` | free | | `spavik.backtest(data, { target, horizon })` | free | | `spavik.forecast(data, { target, horizon })` | `horizon` × the engine's credits per call, all series included; 1 credit per step on the default engine | | `spavik.models()` | free | | `spavik.usage()` | free | | `model.predict(rows)` | the engine's credits per row, 1 on the default engine | | `model.submitOutcomes(outcomes)` | free | | `model.performance(days)` | free | | `model.refresh()` | free | | `model.archive()`, `spavik.archive(modelId)` | free | | `spavik.restore(modelId)` | free | `models()` is an async iterator; `models({ status: 'archived' })` lists what was set aside. A model exposes its `engine` and `features`, the columns `predict` expects. The raw API fields are in `model.data`, for example `model.data.target`. ```js for await (const model of spavik.models()) console.log(model.id, model.name, model.features); ``` ## Engine selection Training and evaluation accept `engineFamily`; omitting it lets the API choose its configured default family. Read `GET /v1/engine-families` for available families. The model's family cannot change. Predictions accept `engineVersion` to select an exact version or latest alias within that family. Forecast and backtest accept both options. Available versions and prices come from `GET /v1/engines`. Keep the exact `engine` from the prediction result with its model `version`. ## Evaluate before training `evaluate` is free and creates nothing. Add `explain: true` to rank every feature column in `report.trial.feature_importance` and choose which ones to keep before training. It runs one extra prediction per column: on a wide table, give the client a longer `timeout`. See [Prepare your data](/guides/data). ```js const report = await spavik.evaluate('churn.csv', { target: 'churn' }); console.log(report.assessment.status); console.log(report.assessment.metric, report.assessment.message); console.log(report.hint); const check = await spavik.backtest('sales.csv', { target: 'sales', horizon: 7, timestamp: 'date', itemId: 'store', }); console.log(check.assessment); console.log(check.backtest); ``` Both operations are free and preserve the API's `assessment`. See [Read an evaluation](/guides/evaluation) for comparison statuses, random-holdout limits and forecasting protocol metadata. A high score does not establish reliability or prove leakage. `backtest()` also accepts `pointEstimate` (`mean` or `median`) and `quantiles`. Use the same settings for backtesting and forecasting so the evaluation matches the forecast you intend to request. ## Configure and forecast ```js const spavik = new Spavik({ apiKey: process.env.SPAVIK_API_KEY, }); const forecast = await spavik.forecast('sales.csv', { target: 'sales', horizon: 7, timestamp: 'date', itemId: 'store', pointEstimate: 'mean', }); console.log(await forecast.points()); // asynchronous method in JavaScript console.log(forecast.csv); console.log(forecast.get('point_estimate'), forecast.predictionsRemaining); ``` `forecast.predictionsRemaining` copies the `X-Spavik-Predictions-Remaining` header, or is `null` when the API does not send it. `forecast.cost` is `null`: the API does not return the amount charged. See [Costs and credits](/costs). `forecast()` also accepts `quantiles` and `pointEstimate: 'median'`. Keep these settings consistent with the backtest you actually ran. Both methods use `itemId` in JavaScript; the REST field is `item_id`. The key travels in `X-Spavik-Api-Key`. If `apiKey` is omitted, the client reads `SPAVIK_API_KEY`; an explicitly empty string disables that fallback. `baseUrl` and `SPAVIK_BASE_URL` are optional overrides. The SDK defaults to `https://api.spavik.co`. Set an override only when needed; `baseUrl` takes precedence. ## Outcomes, refresh and archives ```js const submitted = await model.submitOutcomes(outcomes); console.log(submitted.get('accepted'), submitted.get('refresh_triggered')); const performance = await model.performance(30); const observed = performance?.get('observed'); console.log(observed?.assessment, observed?.metrics, observed?.baselines); console.log(observed?.metric_context, performance?.get('evaluation')); ``` Batch outcomes. When `refresh_triggered` is true, automatic integration has already started; do not request a duplicate refresh. A manual `refresh()` returns the integration details, including `incomplete_outcomes`, or `null` only for `NO_PENDING_OUTCOMES`. Other API failures are raised, including performance-service failures. `observed: null` means there are no client-reported predictions to compare yet. Scores, baselines, undefined reasons and `observed.assessment` come directly from the API; the SDK does not recalculate them. `model.archive()` frees an active-model slot without deleting artifacts. `spavik.restore(modelId)` restores by identifier. Permanent deletion is outside the SDK's surface. See [the feedback loop](/guides/feedback-loop). ## Types The SDK is written in TypeScript and ships compiled JavaScript for ESM and CommonJS, with generated type declarations. JavaScript applications need no TypeScript setup. Autocompletion covers operations, options and error classes. The npm package is named `spavik`. `verify` is exported from `spavik`, and also from `spavik/webhooks` for bundles that want only webhook verification: both paths give the same function. ## The same refusals as Python What the Python library refuses, this one refuses too, with the same message and at the same moment. A conformance suite replays one scenario across both, and across the MCP server. ```js await spavik.train(eightRows, { target: 'churn' }); // SpavikDataError: training needs at least 10 rows, 8 provided. Nothing was sent. ``` ## Errors ```js import { SpavikCreditsExhausted } from 'spavik'; try { await model.predict(rows); } catch (exc) { if (!(exc instanceof SpavikCreditsExhausted)) throw exc; console.error(exc.code, exc.detail, exc.requestId); console.error((await spavik.usage()).predictions_remaining); } ``` Every class extends `SpavikError` and carries `code`, `detail`, `status` and `requestId`: `SpavikDataError`, `SpavikAuthError`, `SpavikCreditsExhausted`, `SpavikQuotaExceeded`, `SpavikValidationError`, `SpavikNotFound`, `SpavikConflict`, `SpavikRateLimited`, `SpavikUnavailable`, `SpavikServerError`, `SpavikTransportError` and `SpavikSignatureError`. They match the Python classes, under the same names. ## Webhooks `verify` checks the `Spavik-Signature` header and returns the decoded event. It rejects with `SpavikSignatureError` when the signature, the timestamp or the header is invalid. Pass the raw body, never a parsed one. See [Webhooks](/guides/webhooks) for the delivery contract. Express: ```js import express from 'express'; import { SpavikSignatureError, verify } from 'spavik'; const app = express(); app.post('/webhooks/spavik', express.raw({ type: 'application/json' }), async (req, res) => { let event; try { event = await verify(req.body, req.headers, process.env.SPAVIK_WEBHOOK_SECRET); } catch (error) { if (error instanceof SpavikSignatureError) return res.sendStatus(400); throw error; } // Store event.id and the event durably, then acknowledge. res.sendStatus(204); }); ``` Hono: ```js import { Hono } from 'hono'; import { SpavikSignatureError, verify } from 'spavik'; const app = new Hono(); app.post('/webhooks/spavik', async (c) => { let event; try { event = await verify(await c.req.arrayBuffer(), c.req.raw.headers, c.env.SPAVIK_WEBHOOK_SECRET); } catch (error) { if (error instanceof SpavikSignatureError) return c.body(null, 400); throw error; } // Store event.id and the event durably, then acknowledge. return c.body(null, 204); }); ``` ## Retries The SDK follows the API retry policy for network failures, eligible 429/5xx responses and `REQUEST_IN_PROGRESS`. It keeps the same idempotency key during internal attempts. Completed responses can be replayed within the API retention window, 24 hours by default from operation creation. Keep `error.idempotencyKey` and `error.requestId` after an uncertain failure. A new SDK method call generates a new key and can repeat an operation that already succeeded. Do not retry blindly or switch keys to bypass `REQUEST_INTERRUPTED` or `IDEMPOTENCY_RESPONSE_UNAVAILABLE`. `maxRetries` counts every attempt, the first included: the default is 3, and `maxRetries: 1` disables retries. Without `Retry-After`, the default backoff is exponential with jitter, from 0.5 s up to 8 s. Each operation has its own timeout, from 15 s for reads to 120 s for training; `timeout`, in milliseconds, applies to calls without one. ```js new Spavik({ maxRetries: 1 }); // a single attempt, no retry new Spavik({ backoff: (n) => n * 1000 }); // linear backoff in ms, n is the attempt number ``` ## Debugging ```sh SPAVIK_LOG=debug node my-script.js ``` # Python SDK (https://docs.spavik.co/libraries/python) Evaluate, train, predict and forecast with the synchronous and asynchronous Python SDK. ```sh pip install spavik # with pandas: pip install "spavik[pandas]" ``` The SDK requires Python 3.11 or later. ```python from spavik import Spavik spavik = Spavik() # reads SPAVIK_API_KEY model = spavik.train("churn.csv", target="churn") # free print(model.predict({"plan": "pro", "seats": 12, "tickets_90d": 3})) ``` Two things to know before the first call: your key, and the name of the column to predict. ## The operations The ones that carry a model identifier live on the `Model` object, so nobody has to retype a `mdl_...` by hand. | Call | Cost | | --- | --- | | `spavik.evaluate(data, target=...)` | free | | `spavik.train(data, target=...)` | free | | `spavik.backtest(data, target=..., horizon=...)` | free | | `spavik.forecast(data, target=..., horizon=...)` | `horizon` × the engine's credits per call, all series included; 1 credit per step on the default engine | | `spavik.models()` | free | | `spavik.usage()` | free | | `model.predict(rows)` | the engine's credits per row, 1 on the default engine | | `model.submit_outcomes(outcomes)` | free | | `model.performance(days=30)` | free | | `model.refresh()` | free | | `model.archive()`, `spavik.archive(model_id)` | free | | `spavik.restore(model_id)` | free | `models()` is an iterator: pagination happens on its own. A model exposes its `engine` and `features`, the columns `predict` expects. The raw API fields are in `model.data`, for example `model.data.get("target")`. ## Engine selection Training and evaluation accept `engine_family`; omitting it lets the API choose its configured default family. Read `GET /v1/engine-families` for available families. The model's family cannot change. Predictions accept `engine_version` to select an exact version or latest alias within that family. Forecast and backtest accept both options. Available versions and prices come from `GET /v1/engines`. Keep the exact `engine` from the prediction result with its model `version`. ## Check before you train, or before you pay `evaluate()` returns structural validation, a trial and the API's `assessment`. `backtest()` validates a series and replays its history against the required references, without returning forecast values. Both are free. `evaluate(..., explain=True)` also ranks every feature column in `report["trial"]["feature_importance"]`, to choose which ones to keep before training. It runs one extra prediction per column: on a wide table, give the client a longer `timeout`. See [Prepare your data](/guides/data). ```python report = spavik.evaluate("churn.csv", target="churn") print(report["assessment"]["status"]) print(report["assessment"]["metric"], report["assessment"]["message"]) print(report.get("hint")) check = spavik.backtest( "sales.csv", target="sales", horizon=7, timestamp="date", point_estimate="median", quantiles=[0.1, 0.5, 0.9], ) print(check["assessment"]) print(check.get("backtest")) ``` Read [the evaluation guide](/guides/evaluation) before interpreting a score. The tabular trial uses a random holdout, with no temporal or group split controls. A better comparison on the sample does not establish future reliability. ## Configuration ```python import os from spavik import Spavik spavik = Spavik( api_key=os.environ["SPAVIK_API_KEY"], ) ``` The key is sent in `X-Spavik-Api-Key`. Omitting `api_key` reads `SPAVIK_API_KEY`; an explicitly empty string disables the environment fallback. `base_url` and `SPAVIK_BASE_URL` are optional overrides. The SDK defaults to `https://api.spavik.co`. Set an override only when needed; `base_url` takes precedence. ## Forecast options and results ```python forecast = spavik.forecast( "sales.csv", target="sales", horizon=7, timestamp="date", point_estimate="median", quantiles=[0.1, 0.5, 0.9], ) print(forecast.points) # parsed rows; no method call in Python print(forecast.csv) print(forecast.get("point_estimate"), forecast.predictions_remaining) ``` `forecast.predictions_remaining` copies the `X-Spavik-Predictions-Remaining` header, or is `None` when the API does not send it. `forecast.cost` is `None`: the API does not return the amount charged. See [Costs and credits](/costs). `item_id` names the series-ID column for multiple series. Match the backtest's history, horizon, column roles, point estimate and quantiles. The API default point estimate is `mean`. See [series preparation](/guides/data#for-a-time-series) for calendar constraints. ## Observe performance ```python report = model.performance(days=30) if report is not None: observed = report.get("observed") if observed is not None: print(observed["assessment"], observed["metrics"], observed["baselines"]) print(observed["metric_context"], report.get("evaluation")) ``` Call this after [reporting outcomes](/guides/feedback-loop). `observed: null` means no client-reported predictions are available for comparison; API failures are raised. The SDK preserves API scores, baselines, undefined reasons and assessment without recalculating them. ## Archive and restore Plans cap the number of active models. `archive()` stops a model from serving and frees its slot; everything is kept, and `spavik.restore(model_id)` brings it back. Restore takes the identifier, not the model: an archived model cannot be read with `model()`, the API answers `MODEL_ARCHIVED`. ```python model.archive() for m in spavik.models(status="archived"): print(m.id, m.name) spavik.restore(model.id) ``` Permanent deletion is not in the library. ## The data format is yours ```python spavik.train(df, target="churn") # pandas DataFrame spavik.train("data/churn.csv", target="churn") # file path spavik.train([{"plan": "pro", "churn": 0}], ...) # list of dicts ``` CSV, JSON, semicolons or commas, quoted or not: the library sorts it out, and retypes numbers along the way. ## What is refused before being sent The library has your data in hand, the API does not yet. Four refusals happen locally, with no network call and no spend. ```python spavik.train(eight_rows, target="churn") # SpavikDataError: training needs at least 10 rows, 8 provided. Nothing was sent. spavik.train(df, target="churn_90d") # SpavikDataError: the target column 'churn_90d' is missing from the data. # Columns found: plan, seats, tickets_90d, churn. Did you mean 'churn'? spavik.forecast(three_points, target="sales", horizon=14) # SpavikDataError: the series has 3 point(s) for a horizon of 14. A series shorter # than its horizon yields nothing usable: provide at least 14 points, # preferably several times more. model.submit_outcomes([{"request_id": "...", "outcome": 1}]) # SpavikDataError: 1 outcome(s) out of 1 have no 'actual' field, the value actually # observed. Fields found on the first: request_id, outcome. ``` The messages are the same, word for word, in the JavaScript library: a conformance suite replays one scenario across both, and across the MCP server. ## Errors One class per family, all subclasses of `SpavikError`, each carrying the machine code, the detail returned by the API, the status and the `request_id`. ```python from spavik import SpavikCreditsExhausted try: model.predict(rows) except SpavikCreditsExhausted as exc: print(exc.code, exc.detail, exc.request_id) print(spavik.usage()["predictions_remaining"]) ``` | Class | When | | --- | --- | | `SpavikDataError` | local refusal, nothing was sent | | `SpavikAuthError` | key missing, invalid, or insufficient | | `SpavikCreditsExhausted` | balance too low; read `usage()` for what is left | | `SpavikQuotaExceeded` | a plan limit is reached: active models, rows | | `SpavikValidationError` | the API refuses the request as formed | | `SpavikNotFound` | the resource does not exist in this workspace | | `SpavikConflict` | replay, work in progress, nothing to integrate | | `SpavikRateLimited` | too many calls | | `SpavikUnavailable` | dependency missing or asleep | | `SpavikServerError` | unexpected API failure | | `SpavikTransportError` | network, or non-JSON response | ## Normal states, not errors `model.refresh()` returns `None` only when there are no pending outcomes. Other API failures are raised. `model.performance()` preserves the API report; `observed: null` means there are no measured outcomes to compare. An API failure is raised rather than converted into an empty performance report. ## Async Same names, same refusals, same results. ```python from spavik import AsyncSpavik async with AsyncSpavik() as spavik: model = await spavik.train("churn.csv", target="churn") print(await model.predict(rows)) ``` ## Retries The SDK follows the API retry policy for network failures, eligible 429/5xx responses and `REQUEST_IN_PROGRESS`. It keeps the same idempotency key during an invocation's internal retries. Replays are protected while the API record is retained (24 hours by default for completed responses). Keep `exc.idempotency_key` and `exc.request_id` after an uncertain failure. Calling the SDK operation again generates a new key and may execute or charge again. `REQUEST_INTERRUPTED` and `IDEMPOTENCY_RESPONSE_UNAVAILABLE` stop automatic retries; inspect the operation before continuing. `max_retries` counts every attempt, the first included: the default is 3, and `max_retries=1` disables retries. Without `Retry-After`, the default backoff is exponential with jitter, from 0.5 s up to 8 s. ```python Spavik(max_retries=1) # a single attempt, no retry Spavik(backoff=lambda n: n * 1.0) # linear backoff, n is the attempt number ``` ## Webhooks `verify` checks the `Spavik-Signature` header and returns the decoded event. It raises `SpavikSignatureError` when the signature, the timestamp or the header is invalid. With Flask: ```python import os from flask import Flask, request from spavik import SpavikSignatureError, verify app = Flask(__name__) @app.post("/spavik/webhook") def spavik_webhook(): try: event = verify(request.get_data(), request.headers, os.environ["SPAVIK_WEBHOOK_SECRET"]) except SpavikSignatureError: return "", 400 # Store event["id"] and the event durably, then acknowledge. return "", 204 ``` The body must be the bytes as received: a parsed and re-serialised body does not produce the same signature. See [Webhooks](/guides/webhooks) for the delivery contract. ## Debugging ```sh SPAVIK_LOG=debug python my_script.py spavik POST /v1/models idem=a3f1b2c4 23.81s 201 req_atZusk... key=sk-spv-api-bm7...Tr9x ``` The key is masked. ## Default timeouts | Operation | Timeout | | --- | --- | | `train`, `evaluate`, `refresh` | 120 s | | `forecast`, `backtest` | 60 s | | `predict`, `submit_outcomes`, `performance`, `archive`, `restore` | 30 s | | `models`, `model`, `usage`, `engines`, `engine_families` | 15 s | The engine takes about ten seconds to answer when it wakes from idle, which is why the timeouts are generous. # MCP server (https://docs.spavik.co/mcp) Connect an agent to twelve Spavik tools for preparation, evaluation, prediction and forecasting. The remote server at `https://mcp.spavik.co` uses stateless Streamable HTTP. There is nothing to install on the server side. Your MCP client must support a custom HTTP header. ## Connect Send your Spavik API key as `X-Spavik-Api-Key`: ```http X-Spavik-Api-Key: sk-spv-api-... ``` For Claude Code, keep the key in the environment: ```sh export SPAVIK_API_KEY="sk-spv-api-..." claude mcp add --transport http spavik https://mcp.spavik.co \ --header "X-Spavik-Api-Key: $SPAVIK_API_KEY" ``` For another client, configure the server URL and the same header. Use its secret input or environment-variable support. Discovery, prompts and the preparation guide are public. For tools and resources that call the API, the MCP forwards only the incoming `X-Spavik-Api-Key`, unchanged. `Authorization` is ignored and is never converted into an API key. The API alone checks credentials, permissions and quotas on the requested operation. The client and its credentials live only for the HTTP request. There is no preliminary key check, credential cache, server-key fallback or OAuth flow. API refusals become MCP tool errors (`isError: true`) or resource errors. ## Prepare before evaluating Start from the business decision and authorized data sources. The agent uses its own connectors to read APIs, files or databases; this MCP has no source connectors. `prepare_data` and `spavik://preparation` guide source inspection, field availability, joins, leakage prevention and a reproducible recipe. Choose classification, regression or forecasting from the decision, not merely from the presence of dates. A future quantity can also use regression when its explanatory features are available at the requested horizon. Explain any unverified temporal or group evaluation requirements. ## The twelve tools | Tool | Purpose | Cost | | --- | --- | --- | | `evaluate_data` | Validate a table and compare a trial with references | Free | | `train_model` | Create and train a reusable model | Free | | `predict` | Score new rows | Engine credits per row, one on the default engine | | `backtest_forecast` | Validate and replay series history | Free | | `forecast` | Forecast future steps without keeping a model | Horizon per call, all series included | | `submit_outcomes` | Report observed outcomes | Free | | `refresh_model` | Integrate pending outcomes into a new version | Free | | `list_models` | List active or archived models | Free | | `model_performance` | Read performance on reported outcomes | Free | | `account_usage` | Read the plan, balance and consumption | Free | | `archive_model` | Stop serving a model and free its quota slot | Free, reversible | | `restore_model` | Restore an archived model | Free | Billable tools describe their pricing. Read `account_usage` for the available balance before a billable call. Cost estimation is unavailable; legacy `dry_run: true` requests are rejected without sending a prediction or forecast. ## Data and parameters Pass file **content** in `data`: CSV with a header row or a JSON array of objects serialized as a string. The remote server cannot read paths on your machine. `predict` and `submit_outcomes` also accept up to 50 objects inline in `rows` and `outcomes` respectively. - `evaluate_data`: `data`, `target`, optional `engine_family` and `explain`. With `explain=true`, `trial.feature_importance` ranks every feature column. - `train_model`: `data`, `target`, optional `name`, `engine_family`, `auto_update`. At most 30 feature columns on public plans, target excluded, as for `evaluate_data`; a wider table is refused with `TOO_MANY_COLUMNS`. The API selects its configured default family when omitted. Keep the returned model ID and feature columns for prediction. - `predict`: `model_id`, `rows` or `data`, optional `engine_version` within the model's family. The response identifies the exact serving engine. - `backtest_forecast`: `data`, `target`, optional `horizon` (default 1), `timestamp`, `item_id`, `point_estimate`, `quantiles`, `engine_family`, `engine_version` and `include_backtest_predictions`. - `forecast`: the same series options and a required `horizon`, with optional `backtest`. `include_backtest_predictions` requires `backtest: true`. `point_estimate` is `mean` by default or `median`. - `submit_outcomes`: `model_id`, and `outcomes` or `data`. Each outcome has `actual` and the full `features`. `request_id` is optional correlation metadata. Add `prediction`, `version` and exact `engine` for performance measurement. - `list_models`: `archived`, `limit` (1 to 50) and `offset` (at least 0). Follow `pagination.next_offset` until null. Set `archived: true` to find models that can be restored. For more than 50 outcomes, send a JSON array as text in `data`, preserving nested `features` and `probabilities`. The API accepts up to 1000 outcomes per batch. CSV is not suitable for these nested objects. Keep horizon, point estimate, quantiles, prepared history and column roles consistent between backtest and forecast. Monthly timestamps must be month-start or month-end. All series must share frequency and calendar anchor, evaluated in UTC. Do not silently shift dates or invent missing observations. ## Read the results Evaluation returns structural `validation` and the API's `assessment` unchanged. A better comparison on this sample does not prove future reliability. See [Read an evaluation](/guides/evaluation) for the statuses, random-holdout limits, MASE weighting, observation counts and exclusions. `model_performance` presents `observed.assessment` and preserves the full API report, including complementary metrics, baselines, per-class and per-version results, undefined reasons and training context. No matched outcomes means no comparison yet. The MCP does not calculate scores or replace the API conclusion. Keep each input row and prediction, including version and engine, to report its observed outcome later. Every outcome requires `features` and `actual`. `submit_outcomes` returns `refresh_triggered`; if true, automatic integration has already started. Do not request a duplicate refresh. A refresh can report `incomplete_outcomes` when some submitted feature rows were incomplete. ## Structured results and safe recovery Starting with MCP server **0.2.0**, every tool publishes an `outputSchema` and returns business fields in `structuredContent.data`. Read `structuredContent.data.assessment` for an evaluation, or `structuredContent.data.predictions`, `.version` and `.engine` after scoring. The human-readable text remains in `content` for existing clients. The common envelope also contains `error`, `request_id`, `idempotency_key`, `idempotent_replay`, `predictions_remaining` and `cost`. Success has `error: null`. `predictions_remaining` copies the API's `X-Spavik-Predictions-Remaining` header (monthly allowance left plus credit balance); null means not reported. A tool failure sets MCP `isError: true`, `data: null` and an error with `code`, `message`, `http_status`, `retryable`, `retry_after_seconds` and `outcome_unknown`. Unknown values remain null; an unknown cost is not zero. Five mutation tools accept `idempotency_key`: `train_model`, `predict`, `forecast`, `submit_outcomes` and `refresh_model`. `archive_model` and `restore_model` reach the same state however often they run, so simply repeat them. Generate a unique key and save it with the tool arguments before the first call. For example, these are the arguments for a one-row prediction: ```json { "model_id": "mdl_your_model", "rows": [{"plan": "pro", "seats": 12, "tickets_90d": 3}], "idempotency_key": "019a00b7-28f6-7000-8000-000000000001" } ``` Use a real model ID and a new key for your own operation. If the response is lost, reuse the same key and arguments with the same workspace credentials. The API can replay completed responses for 24 hours by default, measured from operation creation; an expired completed key can execute and charge again. Respect `retry_after_seconds` and stop when `error.retryable` is false. `REQUEST_INTERRUPTED` and `IDEMPOTENCY_RESPONSE_UNAVAILABLE` require inspection; never create another key to force execution. Keep the request ID for support. A successful replay has `idempotent_replay: true`. The MCP exposes no separate operation-status lookup. If the key is omitted, it generates one for its internal retries, but a client that loses the entire response cannot recover that generated key. Supplying it beforehand prevents that gap. ## Server versions Read `serverInfo.version` from `initialize`, or `version` from `https://mcp.spavik.co/health`, to identify the release available on the endpoint you use. `protocolVersion` identifies the separately negotiated MCP protocol. Version 0.2.0 replaces the former `structuredContent.result` string with the `data` object and result envelope. Update consumers of that structured field; text consumers can keep reading `content`. The current tool surface has twelve tools: `prediction_history` has been removed for every plan. Preserve inputs and results in your application, including model version and exact engine. Version 0.3.0 renames the envelope field `credits_remaining` to `predictions_remaining`, after the header it copies. Clients reading the old name must switch; the value is unchanged. ## Resources and prompts | Resources | Purpose | | --- | --- | | `spavik://preparation` | Public preparation and evaluation guide | | `spavik://operations` | Tool journey and costs | | `spavik://models` | Current workspace models | | `spavik://engines` | Available engine versions and prices | | `spavik://engine-families` | Available families and configured default | The four prompts are `prepare_data`, `build_model`, `score_rows` and `close_the_loop`. Essential guidance also appears in the server instructions and tool descriptions for clients with limited resource or prompt support. ## Model lifecycle Archive a model to free an active-model slot while keeping its artifacts. Restore it when needed; restoration takes a slot again. Account deletion, permanent model deletion, key management, billing, administration and webhooks are outside this MCP's tool surface. # Error codes (https://docs.spavik.co/reference/errors) Machine-readable API errors and retry guidance from the OpenAPI contract. {/* Generated by scripts/sync-openapi.mjs. Do not edit by hand. */} Every API error has `status`, a machine-readable `message` code and a human-readable `messageDetail`. Branch on the code, not on the prose. Keep the `X-Spavik-Request-Id` response header for diagnostics. ## Retry policies Honor `Retry-After` when provided, but check the policy before replaying a mutation. A new idempotency key starts a new logical operation. | Code | Guidance | | --- | --- | | `REQUEST_IN_PROGRESS` | Retry with the same Idempotency-Key and payload after Retry-After. | | `ENGINE_BUSY` | For predictions and forecasts, wait for Retry-After and keep the original Idempotency-Key if supplied. Model creation or refresh may have completed: retry only with the original key and payload; REQUEST_INTERRUPTED requires inspection. Without an original key, inspect model mutations before retrying. | | `ENGINE_UNAVAILABLE` | The engine outcome may be unknown. Retry only with the original Idempotency-Key and payload; otherwise inspect the operation before retrying. | | `USAGE_ACCOUNTING_UNAVAILABLE` | Retry only with the original Idempotency-Key and payload; otherwise contact support before retrying. | | `REQUEST_LOG_UNAVAILABLE` | Retry this GET after Retry-After. | | `REQUEST_INTERRUPTED` | The outcome is uncertain. Do not switch to a new key or retry automatically. Inspect the request log and contact support with X-Spavik-Request-Id. | | `IDEMPOTENCY_RESPONSE_UNAVAILABLE` | The response could not be saved. Do not switch to a new key or retry automatically. Contact support with X-Spavik-Request-Id. | | `IDEMPOTENCY_KEY_REUSED` | Use the original payload for a retry. Use a new key only for a deliberately new operation. | | `CREDITS_EXHAUSTED` | Buy credits or change plan before retrying. | ## Integration error codes These codes cover the documented API-key integrations, including shared authentication, validation and service failures. Not every code applies to every endpoint. Console sign-in, account administration and payment-management errors are excluded. - `ACCOUNT_NOT_FOUND` - `ADMIN_KEY_CANNOT_PREDICT` - `API_KEY_EXPIRED` - `API_KEY_INVALID` - `AUTH_REQUIRED` - `BAD_REQUEST` - `COLUMN_MISMATCH` - `CONTEXT_UNAVAILABLE` - `CREDITS_EXHAUSTED` - `CSV_UNREADABLE` - `DATASET_TOO_LARGE` - `DATA_KEK_INVALID` - `DATA_KEK_MISSING` - `DATA_KEY_MALFORMED` - `DATA_KEY_MISMATCH` - `DATA_KEY_NOT_SENT` - `DATA_KEY_UNREADABLE` - `DATA_KEY_VERSION_UNKNOWN` - `DATA_REQUIRED` - `ENGINE_BUSY` - `ENGINE_CATALOGUE_MISMATCH` - `ENGINE_ERROR` - `ENGINE_FAMILY_MISMATCH` - `ENGINE_FAMILY_ONLY` - `ENGINE_FAMILY_REQUIRED` - `ENGINE_MISMATCH` - `ENGINE_NO_MODEL` - `ENGINE_PLAN_REQUIRED` - `ENGINE_RETIRED` - `ENGINE_UNAVAILABLE` - `FILE_TOO_LARGE` - `FORECAST_FAILED` - `HORIZON_REQUIRED` - `IDEMPOTENCY_KEY_REUSED` - `IDEMPOTENCY_KEY_TOO_LONG` - `IDEMPOTENCY_RESPONSE_UNAVAILABLE` - `INPUT_REQUIRED` - `INTERNAL_ERROR` - `INVALID_CURSOR` - `INVALID_DATE` - `INVALID_ENGINE_VERSION` - `INVALID_INPUT` - `INVALID_RANGE` - `MALFORMED_JSON` - `MALFORMED_PATH` - `MODEL_ARCHIVED` - `MODEL_DELETED` - `MODEL_KEY_MISSING` - `MODEL_NOT_FOUND` - `MODEL_NOT_REFRESHABLE` - `MODEL_NOT_TRAINED` - `NO_PENDING_OUTCOMES` - `NO_ROWS` - `OUTCOME_PREDICTION_INVALID` - `PAYLOAD_TOO_LARGE` - `PERFORMANCE_UNAVAILABLE` - `QUOTA_MODELS_EXCEEDED` - `RANGE_TOO_WIDE` - `REFRESH_IN_PROGRESS` - `REQUEST_ABORTED` - `REQUEST_INTERRUPTED` - `REQUEST_IN_PROGRESS` - `REQUEST_LOG_UNAVAILABLE` - `ROUTE_NOT_FOUND` - `SCHEMA_MISMATCH` - `TARGET_COLUMN_MISSING` - `TARGET_REQUIRED` - `TIMESTAMP_COLUMN_MISSING` - `TIMESTAMP_COLUMN_NOT_FOUND` - `TOO_MANY_COLUMNS` - `TOO_MANY_ROWS` - `UNKNOWN_ENGINE_VERSION` - `UNKNOWN_TASK` - `UNSUPPORTED_CHARSET` - `UNSUPPORTED_ENCODING` - `UPLOAD_INVALID` - `USAGE_ACCOUNTING_UNAVAILABLE` - `USAGE_UNAVAILABLE` - `VALIDATION_ERROR` - `VALIDATION_FAILED` - `WORKSPACE_NOT_FOUND` # Quickstart (https://docs.spavik.co/start) Evaluate a dataset, train a model and make a prediction with Python, JavaScript or REST. ## 1. Configure your API key Create an API key from your workspace and keep it in an environment variable. ```sh export SPAVIK_API_KEY="sk-spv-api-..." ``` Both SDKs default to `https://api.spavik.co`. `SPAVIK_BASE_URL` is optional for SDK calls: set it only to override that default. Both SDKs send the key in `X-Spavik-Api-Key`. See [Authentication](/authentication). Choose your integration: **Python** Install the Python SDK: ```sh pip install spavik ``` **JavaScript** Install the TypeScript SDK, also usable from JavaScript: ```sh npm install spavik ``` **REST (cURL)** No Spavik SDK is needed. The examples send HTTP requests directly to the API. Set the URL used by the cURL examples (shown here for production): ```sh export SPAVIK_BASE_URL="https://api.spavik.co" ``` Run them in a POSIX-compatible shell with these tools available: - **cURL** sends the HTTP requests. - **jq** reads JSON responses and builds JSON request bodies. - **uuidgen** creates unique idempotency keys for requests that need them. Each code group below offers the same operation in Python, JavaScript and REST. Follow one tab throughout the walkthrough. Download [churn.csv](/examples/churn.csv) and save it beside your script. These 300 synthetic customers demonstrate the call format, not production quality. ## 2. Evaluate the table **Python** ```python from spavik import Spavik spavik = Spavik() report = spavik.evaluate("churn.csv", target="churn") print(report["assessment"]) print(report.get("validation")) ``` **JavaScript** ```js import { Spavik } from 'spavik'; const spavik = new Spavik(); const report = await spavik.evaluate('churn.csv', { target: 'churn' }); console.log(report.assessment); console.log(report.validation); ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/datasets/evaluate" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" \ -F 'target=churn' -F 'file=@churn.csv' \ | jq '{assessment, validation}' ``` Evaluation is free and creates nothing. Inspect structural issues and the returned comparison before deciding whether to train. Read [the evaluation guide](/guides/evaluation): the random holdout does not validate every use case, and a better score on this sample does not guarantee future performance. ## 3. Train and predict Once the data is prepared and the evaluation is suitable for your use case, train on the same data and schema. Training is free; the prediction below is billed for one row at the serving engine's credits per row, one on the default engine. See [Costs and credits](/costs). **Python** ```python model = spavik.train("churn.csv", target="churn") result = model.predict({"plan": "pro", "seats": 12, "tickets_90d": 3}) print(model.id, model.features) print(result.predictions) ``` **JavaScript** ```js const model = await spavik.train('churn.csv', { target: 'churn' }); const result = await model.predict({ plan: 'pro', seats: 12, tickets_90d: 3 }); console.log(model.id, model.features); console.log(result.predictions); ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/models" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -F 'target=churn' -F 'file=@churn.csv' \ > training.json MODEL_ID=$(jq -er '.model.id' training.json) jq '.model | {id, feature_columns}' training.json curl --fail-with-body "$SPAVIK_BASE_URL/v1/models/$MODEL_ID/predict" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H 'Content-Type: application/json' \ --data '{"rows":[{"plan":"pro","seats":12,"tickets_90d":3}]}' \ > prediction.json jq '.predictions' prediction.json ``` The API selects its configured default family when `engine_family` is omitted, through REST and both SDKs. Read `GET /v1/engine-families` to choose another family; the SDK option is `engine_family` in Python and `engineFamily` in JavaScript. Training requires at least ten labelled rows and target variation; those minimums do not establish quality. ## 4. Keep the prediction inputs and results Store each input row and returned prediction alongside your business identifier, including its version, engine and optional `request_id`. When the observed outcome becomes available, report the input in `features` and the outcome in `actual`. Never invent an outcome just to exercise the update loop. See [the feedback loop](/guides/feedback-loop) for examples in all three integrations. ## For a time series Forecasting keeps no model. Backtest the intended horizon first (free), inspect the assessment, then decide whether to pay for a forecast. Download [sales.csv](/examples/sales.csv), six months of synthetic daily sales, to try it. **Python** ```python check = spavik.backtest("sales.csv", target="sales", timestamp="date", horizon=7) print(check["assessment"]) # After reviewing the evaluation, this call costs 7 credits for one series: forecast = spavik.forecast("sales.csv", target="sales", timestamp="date", horizon=7) print(forecast.points) ``` **JavaScript** ```js const check = await spavik.backtest('sales.csv', { target: 'sales', timestamp: 'date', horizon: 7, }); console.log(check.assessment); // After reviewing the evaluation, this call costs 7 credits for one series: const forecast = await spavik.forecast('sales.csv', { target: 'sales', timestamp: 'date', horizon: 7, }); console.log(await forecast.points()); ``` **REST (cURL)** ```sh curl --fail-with-body "$SPAVIK_BASE_URL/v1/forecast/backtest" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" \ -F 'file=@sales.csv' -F 'target=sales' -F 'timestamp=date' -F 'horizon=7' \ | jq '.assessment' # After reviewing the evaluation, this call costs 7 credits: curl --fail-with-body "$SPAVIK_BASE_URL/v1/forecast" \ -H "X-Spavik-Api-Key: $SPAVIK_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -F 'file=@sales.csv' -F 'target=sales' -F 'timestamp=date' -F 'horizon=7' \ | jq -r '.forecast_csv' ``` Replace `sales.csv` with your own observed series. See [Preparing your data](/guides/data) for regular intervals, monthly dates and multiple series. # Shared OpenAPI components The operation contracts above resolve their #/components/ references against this shared object. ```json { "components": { "securitySchemes": { "apiKey": { "type": "apiKey", "in": "header", "name": "X-Spavik-Api-Key", "description": "Spavik integration API key (sk-spv-api-...), sent in X-Spavik-Api-Key." } }, "schemas": { "MetricContext": { "type": "object", "additionalProperties": false, "required": [ "undefined_reasons" ], "properties": { "undefined_reasons": { "type": "object", "additionalProperties": { "type": "string", "nullable": true }, "description": "One reason per undefined score; null when the score is defined. Missing probabilities are never reconstructed from hard labels." }, "class_order": { "type": "array", "items": { "type": "string" } }, "positive_class": { "type": "string", "nullable": true }, "auc_average": { "type": "string", "enum": [ "binary", "macro_ovr" ] }, "average_precision_method": { "type": "string", "enum": [ "non_interpolated" ] }, "macro_f1_average": { "type": "string", "enum": [ "unweighted_defined_classes" ] }, "log_loss_epsilon": { "type": "number", "example": 2.220446049250313e-16 } } }, "IntervalMetrics": { "type": "object", "nullable": true, "additionalProperties": false, "description": "Coverage and width on the same backtest observations, using the widest available quantile pair. Fractions use 0 to 1. Width is in target units, never an implicit percentage. Descriptive measurements do not establish calibration.", "required": [ "nominal", "observed", "lower", "upper", "evaluated_count", "average_width", "width_unit", "undefined_reason" ], "properties": { "nominal": { "type": "number", "minimum": 0, "maximum": 1 }, "observed": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1 }, "lower": { "type": "number", "minimum": 0, "maximum": 1 }, "upper": { "type": "number", "minimum": 0, "maximum": 1 }, "evaluated_count": { "type": "integer", "minimum": 0 }, "average_width": { "type": "number", "nullable": true, "minimum": 0 }, "width_unit": { "type": "string", "enum": [ "target" ] }, "undefined_reason": { "type": "string", "nullable": true } } }, "BacktestPredictions": { "type": [ "array", "null" ], "items": { "$ref": "#/components/schemas/BacktestPrediction" }, "description": "Historical observations already evaluated; null unless include_backtest_predictions=true and a backtest ran. One row per n_eval point, ordered oldest window first, then series and timestamp. For MASE, also use evaluation.series[].mase_scale and equal weighting per eligible series. These are not future forecasts." }, "BacktestPrediction": { "type": "object", "required": [ "series", "window", "timestamp", "actual", "prediction", "naive_prediction", "constant_prediction", "quantiles" ], "properties": { "series": { "type": "string", "description": "Original series identifier, or series when no item_id was supplied." }, "window": { "type": "integer", "minimum": 0, "description": "0 is the most recent window. (series, window) identifies the matching evaluation.windows entry." }, "timestamp": { "type": "string", "description": "ISO timestamp in UTC without a timezone suffix." }, "actual": { "type": "number" }, "prediction": { "type": "number", "description": "Point estimate selected by point_estimate." }, "naive_prediction": { "type": "number" }, "constant_prediction": { "type": "number", "description": "Median of the history before this window." }, "quantiles": { "type": "object", "additionalProperties": { "type": "number" }, "description": "Quantile level to prediction, e.g. 0.1 and 0.9." } } }, "Error": { "type": "object", "properties": { "status": { "type": "string", "example": "error" }, "message": { "type": "string", "enum": [ "ACCOUNT_NOT_FOUND", "ADMIN_KEY_CANNOT_PREDICT", "API_KEY_EXPIRED", "API_KEY_INVALID", "AUTH_REQUIRED", "BAD_REQUEST", "COLUMN_MISMATCH", "CONTEXT_UNAVAILABLE", "CREDITS_EXHAUSTED", "CSV_UNREADABLE", "DATASET_TOO_LARGE", "DATA_KEK_INVALID", "DATA_KEK_MISSING", "DATA_KEY_MALFORMED", "DATA_KEY_MISMATCH", "DATA_KEY_NOT_SENT", "DATA_KEY_UNREADABLE", "DATA_KEY_VERSION_UNKNOWN", "DATA_REQUIRED", "ENGINE_BUSY", "ENGINE_CATALOGUE_MISMATCH", "ENGINE_ERROR", "ENGINE_FAMILY_MISMATCH", "ENGINE_FAMILY_ONLY", "ENGINE_FAMILY_REQUIRED", "ENGINE_MISMATCH", "ENGINE_NO_MODEL", "ENGINE_PLAN_REQUIRED", "ENGINE_RETIRED", "ENGINE_UNAVAILABLE", "FILE_TOO_LARGE", "FORECAST_FAILED", "HORIZON_REQUIRED", "IDEMPOTENCY_KEY_REUSED", "IDEMPOTENCY_KEY_TOO_LONG", "IDEMPOTENCY_RESPONSE_UNAVAILABLE", "INPUT_REQUIRED", "INTERNAL_ERROR", "INVALID_CURSOR", "INVALID_DATE", "INVALID_ENGINE_VERSION", "INVALID_INPUT", "INVALID_RANGE", "MALFORMED_JSON", "MALFORMED_PATH", "MODEL_ARCHIVED", "MODEL_DELETED", "MODEL_KEY_MISSING", "MODEL_NOT_FOUND", "MODEL_NOT_REFRESHABLE", "MODEL_NOT_TRAINED", "NO_PENDING_OUTCOMES", "NO_ROWS", "OUTCOME_PREDICTION_INVALID", "PAYLOAD_TOO_LARGE", "PERFORMANCE_UNAVAILABLE", "QUOTA_MODELS_EXCEEDED", "RANGE_TOO_WIDE", "REFRESH_IN_PROGRESS", "REQUEST_ABORTED", "REQUEST_INTERRUPTED", "REQUEST_IN_PROGRESS", "REQUEST_LOG_UNAVAILABLE", "ROUTE_NOT_FOUND", "SCHEMA_MISMATCH", "TARGET_COLUMN_MISSING", "TARGET_REQUIRED", "TIMESTAMP_COLUMN_MISSING", "TIMESTAMP_COLUMN_NOT_FOUND", "TOO_MANY_COLUMNS", "TOO_MANY_ROWS", "UNKNOWN_ENGINE_VERSION", "UNKNOWN_TASK", "UNSUPPORTED_CHARSET", "UNSUPPORTED_ENCODING", "UPLOAD_INVALID", "USAGE_ACCOUNTING_UNAVAILABLE", "USAGE_UNAVAILABLE", "VALIDATION_ERROR", "VALIDATION_FAILED", "WORKSPACE_NOT_FOUND" ], "example": "QUOTA_MODELS_EXCEEDED", "description": "Machine-readable code (UPPER_SNAKE), stable: this is what a client branches on." }, "messageDetail": { "type": "string", "example": "Model limit reached (3/3) on the build plan. Archive a model you no longer serve, or upgrade to the pro plan.", "description": "The whole story, for a human: what was refused and what to do about it." } }, "required": [ "status", "message", "messageDetail" ] }, "ForecastResult": { "type": "object", "properties": { "forecast_csv": { "type": "string" }, "target": { "type": "string", "nullable": true }, "prediction_length": { "type": "integer", "nullable": true }, "n_series": { "type": "integer", "nullable": true }, "n_context": { "type": "integer", "nullable": true }, "point_estimate": { "type": "string", "nullable": true, "enum": [ "mean", "median" ], "description": "Statistic served in the _prediction column. With median it matches quantile_0.5 exactly; with mean it differs on a skewed distribution." }, "metrics": { "type": "object", "nullable": true, "additionalProperties": { "type": "number", "nullable": true }, "description": "Scores on evaluated observations. Classification: AUC, Accuracy, AveragePrecision (binary only), LogLoss and MacroF1. Regression: MAE, RMSE, R². Forecasting adds MASE, Bias, BiasRatio and Pinball. Undefined scores are null; see metric_context.undefined_reasons. Secondary scores do not change the primary assessment.", "properties": { "AUC": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "ROC AUC, binary positive class or multiclass one-vs-rest macro average." }, "Accuracy": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1 }, "AveragePrecision": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Binary non-interpolated average precision, not trapezoidal PR-AUC. Null without positive observations." }, "LogLoss": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean negative log probability of the actual class, natural logarithm, float64 epsilon clipping. Lower is better." }, "MacroF1": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Unweighted mean of defined per-class F1 scores. Classes absent from both actual and predicted labels are excluded." }, "MAE": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean absolute error in target units." }, "RMSE": { "type": "number", "nullable": true, "minimum": 0 }, "R²": { "type": "number", "nullable": true, "description": "Null for fewer than two observations or constant actual values. Can be negative." }, "MASE": { "type": "number", "nullable": true, "minimum": 0, "description": "Compare with the scored naive baseline on the same backtest, not with a fixed threshold of 1." }, "Bias": { "type": "number", "nullable": true }, "BiasRatio": { "type": "number", "nullable": true }, "Pinball": { "type": "number", "nullable": true, "minimum": 0 } } }, "baselines": { "type": "object", "additionalProperties": true, "nullable": true, "description": "Naive baselines (seasonal when selected from the initial training context) measured on the same evaluated points as metrics. MASE also includes constant_floor and constant_label for the historical median estimated before each window. Model, naive and constant share the same scale computed before the earliest evaluation window. Compare MASE to these scores, never to the fixed threshold of 1." }, "seasonal_period": { "type": "integer", "nullable": true }, "backtest": { "type": "object", "additionalProperties": true, "nullable": true, "description": "Score per origin: mase_by_window gives the MASE of each window, naive_mase_by_window and constant_mase_by_window the matching baseline scores. Window indices are relative to each series end and may represent different dates. Protocol v2 MASE weights eligible series equally, then points within each series. Other metrics weight points equally. Do not reconstruct the aggregate by averaging window scores.", "properties": { "predictions": { "$ref": "#/components/schemas/BacktestPredictions" } } }, "backtest_skipped": { "type": "string", "nullable": true, "description": "Set when a requested backtest could not run (series too short)." }, "intervals": { "$ref": "#/components/schemas/IntervalMetrics" }, "evaluation": { "type": "object", "nullable": true, "additionalProperties": true, "properties": { "metric_context": { "$ref": "#/components/schemas/MetricContext" } } }, "warnings": { "type": "array", "nullable": true, "items": { "type": "string" } }, "engine": { "type": "string", "nullable": true } } }, "ClassificationReport": { "type": [ "object", "null" ], "additionalProperties": false, "description": "Per-class scores on actual predicted labels. Null for regression, older engines or unavailable evaluation. At training these describe the holdout; an augmented version retains its initial evaluation. In performance these describe only predictions matched to received outcomes, not the entire prediction population. All classes known to the evaluated versions and all observed/predicted labels are included. The AUC assessment is unchanged.", "required": [ "evaluated_count", "decision_rule", "per_class" ], "properties": { "evaluated_count": { "type": "integer", "minimum": 0, "description": "Number of rows evaluated. Each row contributes once to support and once to predicted_count." }, "decision_rule": { "$ref": "#/components/schemas/ClassificationDecisionRule" }, "per_class": { "type": "array", "items": { "$ref": "#/components/schemas/ClassificationClassMetrics" } } } }, "ClassificationDecisionRule": { "type": "object", "additionalProperties": false, "required": [ "kind", "description", "class_order", "threshold", "tie_break" ], "properties": { "kind": { "type": "string", "enum": [ "argmax", "model_predict", "recorded_prediction" ] }, "description": { "type": "string", "description": "Actual decision rule. TabICL uses the largest class probability, first class in class_order on ties, without a custom threshold. model_predict means estimator details were unavailable. recorded_prediction uses the class originally announced; no threshold is reapplied to historical outcomes." }, "class_order": { "type": "array", "items": { "type": "string" }, "description": "Probability column order when known; empty for reports combining recorded decisions." }, "threshold": { "type": [ "number", "null" ], "description": "Custom probability threshold if used; null for the current argmax rule or unavailable historical rule." }, "tie_break": { "type": [ "string", "null" ], "enum": [ "first_in_class_order", null ] } } }, "ClassificationClassMetrics": { "type": "object", "additionalProperties": false, "required": [ "class", "support", "predicted_count", "true_positive", "precision", "recall", "f1", "undefined_reasons" ], "properties": { "class": { "type": "string", "description": "Class label serialized as a string." }, "support": { "type": "integer", "minimum": 0, "description": "Actual occurrences of this class in the evaluated rows." }, "predicted_count": { "type": "integer", "minimum": 0, "description": "Evaluated rows predicted as this class." }, "true_positive": { "type": "integer", "minimum": 0, "description": "Rows where the actual and predicted labels both equal this class." }, "precision": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "true_positive / predicted_count; null when predicted_count is zero." }, "recall": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "true_positive / support; null when support is zero." }, "f1": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "2 * true_positive / (support + predicted_count); null only if both counts are zero. A present but never predicted class has F1 zero." }, "undefined_reasons": { "type": "object", "additionalProperties": false, "required": [ "precision", "recall", "f1" ], "description": "Null reason means the corresponding score is defined, including a valid zero.", "properties": { "precision": { "type": [ "string", "null" ], "enum": [ "no_predicted_samples", null ] }, "recall": { "type": [ "string", "null" ], "enum": [ "no_actual_samples", null ] }, "f1": { "type": [ "string", "null" ], "enum": [ "no_actual_or_predicted_samples", null ] } } } } }, "Calibration": { "type": [ "object", "null" ], "description": "Descriptive calibration on held-out test rows, before the final persistence fit. Does not recalibrate probabilities. Null for regression or missing historical diagnostics. Retained as the initial evaluation on same-engine refreshes; cleared when the engine changes.", "required": [ "available", "unavailable_reason", "evaluated_count", "evaluated_on", "kind", "interpretation", "binning", "n_bins", "bin_boundaries", "per_class" ], "properties": { "available": { "type": "boolean", "description": "Whether the diagnostic can be computed, not whether probabilities are reliable." }, "unavailable_reason": { "type": [ "string", "null" ], "enum": [ "no_evaluated_samples", "insufficient_classes", "missing_probabilities", "invalid_probabilities", "unknown_actual_class", null ] }, "evaluated_count": { "type": "integer", "minimum": 0 }, "evaluated_on": { "type": "string", "enum": [ "test" ] }, "kind": { "type": "string", "enum": [ "one_vs_rest" ] }, "interpretation": { "type": "string", "enum": [ "descriptive_only" ] }, "binning": { "type": "string", "enum": [ "uniform" ] }, "n_bins": { "type": "integer", "enum": [ 10 ] }, "bin_boundaries": { "type": "string", "enum": [ "left_closed_right_open_last_closed" ] }, "per_class": { "type": "array", "description": "Model class order. Each class uses every test row. Empty when unavailable.", "items": { "type": "object", "required": [ "class", "bins" ], "properties": { "class": { "type": "string" }, "bins": { "type": "array", "minItems": 10, "maxItems": 10, "items": { "type": "object", "required": [ "lower", "upper", "count", "positive_count", "mean_predicted_probability", "observed_frequency" ], "properties": { "lower": { "type": "number", "minimum": 0, "maximum": 1 }, "upper": { "type": "number", "minimum": 0, "maximum": 1 }, "count": { "type": "integer", "minimum": 0 }, "positive_count": { "type": "integer", "minimum": 0 }, "mean_predicted_probability": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "Null for an empty bin." }, "observed_frequency": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1, "description": "positive_count / count; null for an empty bin." } } } } } } } } }, "Model": { "type": "object", "properties": { "id": { "type": "string", "example": "mdl_7Kq2Vn5RtWz3Bx8Ldm" }, "name": { "type": "string" }, "slug": { "type": "string", "nullable": true }, "engine_family": { "type": "string", "enum": [ "flair", "presage", "prophecy" ] }, "target": { "type": "string", "nullable": true }, "task": { "type": "string", "enum": [ "classification", "regression" ], "nullable": true }, "engine": { "type": "string", "nullable": true, "description": "Engine that prepared and evaluated this data version; not a prediction engine lock." }, "feature_columns": { "type": "array", "items": { "type": "string" }, "nullable": true, "description": "Columns the served version expects in each row sent to predict. Null until the first training." }, "status": { "type": "string", "enum": [ "draft", "training", "ready", "failed", "archived" ], "description": "ready serves predictions. training: the first version is being produced; a refresh keeps the model ready. failed: the first training failed and no version can serve. archived: set aside, predictions return 409 MODEL_ARCHIVED until it is restored. draft is transient, before the first training starts. A deleted model is no longer returned (404 MODEL_NOT_FOUND)." }, "auto_update": { "type": "boolean", "description": "Automatically folds outcomes back into the model." }, "retain_outcomes": { "type": "boolean", "deprecated": true, "description": "Deprecated compatibility field, always true. Outcomes are functional model data and require no retention opt-in." }, "latest_version": { "type": "integer" }, "created_at": { "type": "string", "format": "date-time" } } }, "Pagination": { "type": "object", "properties": { "total": { "type": "integer" }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "has_more": { "type": "boolean" } } }, "TrainingResult": { "type": "object", "description": "The model created, the version produced (n_train, metrics, warnings) and the data validation report. The version is not repeated inside the model.", "properties": { "model": { "$ref": "#/components/schemas/Model" }, "version": { "$ref": "#/components/schemas/ModelVersion" }, "validation": { "type": "object", "additionalProperties": true } } }, "ModelVersion": { "type": "object", "properties": { "id": { "type": "string", "example": "ver_2Hj9PxLm4Nc6Tq8Rvb" }, "version": { "type": "integer" }, "status": { "type": "string", "enum": [ "training", "ready", "failed" ], "description": "training: being produced. ready: usable for predictions. failed: never served." }, "target": { "type": "string" }, "task": { "type": "string" }, "n_train": { "type": "integer", "nullable": true }, "n_sampled_from": { "type": "integer", "nullable": true, "description": "Usable rows in the uploaded file. Higher than n_train, it reveals sub-sampling (max_samples, or engine memory)." }, "metrics": { "type": "object", "nullable": true, "additionalProperties": { "type": "number", "nullable": true }, "description": "Scores on evaluated observations. Classification: AUC, Accuracy, AveragePrecision (binary only), LogLoss and MacroF1. Regression: MAE, RMSE, R². Forecasting adds MASE, Bias, BiasRatio and Pinball. Undefined scores are null; see metric_context.undefined_reasons. Secondary scores do not change the primary assessment.", "properties": { "AUC": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "ROC AUC, binary positive class or multiclass one-vs-rest macro average." }, "Accuracy": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1 }, "AveragePrecision": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Binary non-interpolated average precision, not trapezoidal PR-AUC. Null without positive observations." }, "LogLoss": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean negative log probability of the actual class, natural logarithm, float64 epsilon clipping. Lower is better." }, "MacroF1": { "type": "number", "nullable": true, "minimum": 0, "maximum": 1, "description": "Unweighted mean of defined per-class F1 scores. Classes absent from both actual and predicted labels are excluded." }, "MAE": { "type": "number", "nullable": true, "minimum": 0, "description": "Mean absolute error in target units." }, "RMSE": { "type": "number", "nullable": true, "minimum": 0 }, "R²": { "type": "number", "nullable": true, "description": "Null for fewer than two observations or constant actual values. Can be negative." }, "MASE": { "type": "number", "nullable": true, "minimum": 0, "description": "Compare with the scored naive baseline on the same backtest, not with a fixed threshold of 1." }, "Bias": { "type": "number", "nullable": true }, "BiasRatio": { "type": "number", "nullable": true }, "Pinball": { "type": "number", "nullable": true, "minimum": 0 } } }, "baselines": { "type": "object", "additionalProperties": true, "nullable": true }, "classification_report": { "$ref": "#/components/schemas/ClassificationReport" }, "calibration": { "$ref": "#/components/schemas/Calibration" }, "evaluation": { "type": "object", "nullable": true, "additionalProperties": true, "description": "Initial holdout protocol, split and sample context. Retained across context refreshes; not a new evaluation.", "properties": { "metric_context": { "$ref": "#/components/schemas/MetricContext" } } }, "warnings": { "type": "array", "nullable": true, "items": { "type": "string" }, "description": "What the engine agreed to do but flagged: context trimmed to fit in memory, classes or columns outside the optimised range. Worth reading when a model disappoints and nothing else explains it." }, "engine": { "type": "string", "nullable": true, "description": "Engine that prepared this context and produced its evaluation (e.g. presage-2). Predictions may select another available version in the same family." } } }, "ScoringResult": { "type": "object", "properties": { "model_id": { "type": "string", "example": "mdl_7Kq2Vn5RtWz3Bx8Ldm" }, "version": { "type": "integer" }, "engine": { "type": "string", "example": "presage-2", "description": "Exact engine used for this prediction." }, "n_scored": { "type": "integer" }, "took_ms": { "type": "integer", "description": "Total time on the Spavik side." }, "predictions": { "type": "array", "description": "One entry per row sent, in the same order. `prediction` is a number for regression, and for classification the predicted class (a number when the classes are numeric, text otherwise). Classification also returns one probability per class (`proba_`).", "items": { "type": "object", "properties": { "request_id": { "type": "string", "example": "req_5Zt8Wn3QmXd7Bp2Kfh", "description": "Prediction identifier to send with outcomes. Distinct from the HTTP X-Spavik-Request-Id header." }, "prediction": { "oneOf": [ { "type": "number" }, { "type": "string" } ], "example": 1 } }, "additionalProperties": { "type": "number", "description": "proba_" }, "example": { "request_id": "req_5Zt8Wn3QmXd7Bp2Kfh", "prediction": 1, "proba_0": 0.2, "proba_1": 0.8 } } } } }, "Usage": { "type": "object", "properties": { "plan": { "type": "string", "enum": [ "free", "build", "pro", "growth", "enterprise" ] }, "credit_balance": { "type": "integer", "description": "Prediction balance: purchased, refunded or granted by staff, as a single number. Spent after the monthly allowance, and never expires." }, "credits_low": { "type": "boolean" }, "predictions_remaining": { "type": "integer", "nullable": true, "description": "What can still be consumed: monthly allowance left plus balance. The same number as the X-Spavik-Predictions-Remaining header on billable responses. Null on an unlimited plan." }, "unlimited_usage": { "type": "boolean", "description": "True only when unlimited usage is explicitly configured. An absent balance alone does not imply unlimited usage." }, "included_units_per_month": { "type": "integer", "nullable": true }, "period": { "type": "object", "properties": { "key": { "type": "string", "example": "2026-09" }, "reset_at": { "type": "string", "format": "date-time", "nullable": true }, "units_used": { "type": "integer", "description": "Monthly allowance already spent." } } }, "window": { "type": "object", "properties": { "from": { "type": "string", "format": "date" }, "to": { "type": "string", "format": "date" } } }, "totals": { "$ref": "#/components/schemas/UsageCounts" }, "daily": { "type": "array", "items": { "allOf": [ { "type": "object", "properties": { "date": { "type": "string", "format": "date" } } }, { "$ref": "#/components/schemas/UsageCounts" } ] } }, "by_kind": { "type": "array", "items": { "$ref": "#/components/schemas/UsageBreakdown" } }, "by_workspace": { "type": "array", "items": { "$ref": "#/components/schemas/UsageBreakdown" } }, "by_api_key": { "type": "array", "items": { "$ref": "#/components/schemas/UsageBreakdown" } } } }, "UsageCounts": { "type": "object", "properties": { "units": { "type": "integer", "description": "Credits billed: scored rows times engine price for model predictions, or requested horizon per forecast call with all series included. This is the billed usage.", "example": 8 }, "predictions": { "type": "integer", "description": "Predictions produced: rows scored, or horizon x series forecast.", "example": 2 } }, "required": [ "units", "predictions" ] }, "UsageBreakdown": { "allOf": [ { "type": "object", "properties": { "key": { "type": "string", "description": "Identifier of the row: kind, workspace or API key." }, "label": { "type": "string", "nullable": true } } }, { "$ref": "#/components/schemas/UsageCounts" } ] }, "RequestLog": { "type": "object", "properties": { "window": { "type": "object", "additionalProperties": true }, "n_requests": { "type": "integer", "description": "Calls in this page." }, "next_cursor": { "type": "string", "nullable": true, "description": "Pass it as cursor to read the next, older page. Null on the last page." }, "requests": { "type": "array", "items": { "type": "object", "properties": { "request_id": { "type": "string" }, "at": { "type": "string", "format": "date-time" }, "method": { "type": "string", "example": "POST" }, "path": { "type": "string", "example": "/v1/models/mdl_7Kq2Vn5RtWz3Bx8Ldm/predict" }, "status": { "type": "integer", "example": 402 }, "error": { "type": "object", "nullable": true, "properties": { "code": { "type": "string", "example": "CREDITS_EXHAUSTED" }, "detail": { "type": "string", "nullable": true } } }, "duration_ms": { "type": "integer", "description": "API processing time from request journaling middleware to response preparation, including engine and accounting. Excludes request body parsing, the final request-log write and network transit; not isolated inference time." }, "workspace_id": { "type": "string", "example": "ws_3v8QpZ2mKdRt7Lc9Xw" }, "api_key_id": { "type": "string", "example": "key_5Zt8Wn3QmXd7Bp2Kfh", "nullable": true }, "billing_user_id": { "type": "string", "description": "Account billed for this workspace, resolved by the API." }, "actor_user_id": { "type": "string", "nullable": true, "description": "Authenticated user for a JWT session. Null for API keys and historical entries without attribution; not the key creator." }, "auth_method": { "type": "string", "enum": [ "jwt", "api_key" ], "nullable": true }, "client_ip": { "type": "string", "nullable": true, "description": "IP resolved from the connection and configured trusted proxies. A gateway, relay or MCP server may be the observed caller. Null if unavailable or invalid." }, "user_agent": { "type": "string", "nullable": true, "maxLength": 1024, "description": "Information about the agent originating the request, when known. Browsers supply their own value. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, "client_name": { "type": "string", "nullable": true, "maxLength": 128, "description": "HTTP caller: SDK, MCP server or direct application (for example spavik-sdk-python, spavik-sdk-javascript or spavik-mcp). Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." }, "client_version": { "type": "string", "nullable": true, "maxLength": 64, "description": "Version of that SDK, MCP server or direct application. Optional diagnostic metadata, declared by the caller and never used for authorization. Omit unknown values. Values are trimmed and truncated to the documented length." } } } } } }, "Plan": { "type": "object", "properties": { "id": { "type": "string", "enum": [ "free", "build", "pro", "growth", "enterprise" ], "example": "pro" }, "name": { "type": "string", "example": "Pro" }, "prices": { "type": "array", "items": { "$ref": "#/components/schemas/PlanPrice" }, "description": "Active tariffs only. Empty for Free, negotiated plans, or an offer awaiting Stripe configuration." }, "currency": { "type": "string", "enum": [ "USD" ] }, "price_excludes_tax": { "type": "boolean", "enum": [ true ] }, "price_per_thousand_cents": { "type": "integer", "nullable": true, "example": 597, "description": "Top-up price for 1,000 credits, in cents and excluding tax. Null for a negotiated contract." }, "description": { "type": "string", "example": "For products that rely on prediction every day. Higher limits, advanced production capabilities, and Zero Data Retention." }, "data_policy": { "$ref": "#/components/schemas/PlanDataPolicy" }, "advanced_data_controls": { "type": "boolean", "nullable": true, "description": "Advanced governance controls in the offer. Null for negotiated terms." }, "support_tier": { "type": "string", "nullable": true, "example": "email" }, "signup_credits": { "type": "integer", "example": 0, "description": "One-time signup credits offered by Free. Zero for paid plans: subscribing does not grant another signup bonus." }, "limits": { "type": "object", "properties": { "max_context_rows": { "type": "integer", "nullable": true, "example": 2000, "description": "Maximum learned context rows. Larger tables are sampled down to this, never refused. Null for a negotiated contract." }, "max_feature_columns": { "type": "integer", "nullable": true, "example": 30, "description": "Maximum feature columns, target excluded, for evaluation and training. Wider tables are refused with 400 TOO_MANY_COLUMNS. Null for a negotiated contract." }, "max_predict_rows": { "type": "integer", "nullable": true, "example": 1000, "description": "Maximum rows per prediction request. Null for a negotiated contract." }, "max_models": { "type": "integer", "nullable": true, "example": 10, "description": "Maximum active models across all workspaces of the account. Null for a negotiated contract." }, "included_units_per_month": { "type": "integer", "nullable": true, "example": 30000, "description": "Credits included every month, Free included (100, reset on the first of each calendar month, UTC). Paid plans reset on their subscription cycle. null means no configured allowance and does not by itself grant unlimited usage." }, "unlimited_usage": { "type": "boolean", "description": "Whether usage is explicitly unlimited. False by default, including Enterprise." } }, "required": [ "max_context_rows", "max_feature_columns", "max_predict_rows", "max_models", "included_units_per_month", "unlimited_usage" ] } }, "required": [ "id", "name", "prices", "currency", "price_excludes_tax", "price_per_thousand_cents", "description", "data_policy", "advanced_data_controls", "support_tier", "signup_credits", "limits" ] }, "PlanPrice": { "type": "object", "required": [ "id", "billing_interval", "amount_cents", "currency", "price_excludes_tax" ], "properties": { "id": { "type": "string", "format": "uuid" }, "billing_interval": { "type": "string", "enum": [ "month", "year" ] }, "amount_cents": { "type": "integer", "minimum": 1, "description": "Full amount charged per billing interval, excluding tax. An annual amount is not a monthly equivalent." }, "currency": { "type": "string", "enum": [ "USD" ] }, "price_excludes_tax": { "type": "boolean", "enum": [ true ] } } }, "PlanDataPolicy": { "type": "string", "enum": [ "training_allowed", "no_training", "zero_data_retention", "custom" ], "description": "Data handling policy associated with the offer. training_allowed permits data reuse for global Spavik training; no_training prohibits that reuse; zero_data_retention disables the archiving of inputs and outputs for evaluate, backtest, forecast and predict; routes, request bodies, responses, idempotency, model state and outcomes behave identically under every data policy; custom requires negotiated terms." }, "WebhookEventModelTrained": { "type": "object", "required": [ "id", "type", "created_at", "workspace_id", "data" ], "properties": { "id": { "type": "string", "description": "Stable event identifier. Deduplicate by this id for each receiver." }, "type": { "type": "string", "enum": [ "model.trained" ] }, "created_at": { "type": "string", "format": "date-time" }, "workspace_id": { "type": "string" }, "data": { "type": "object", "required": [ "model_id", "version", "target", "task", "n_train", "metrics" ], "properties": { "model_id": { "type": "string" }, "version": { "type": "integer" }, "target": { "type": "string" }, "task": { "type": "string", "enum": [ "classification", "regression" ] }, "n_train": { "type": "integer" }, "metrics": { "type": "object", "nullable": true, "additionalProperties": true } } } }, "example": { "id": "evt_example", "type": "model.trained", "created_at": "2026-10-11T12:00:00.000Z", "workspace_id": "ws_example", "data": { "model_id": "mdl_example", "version": 1, "target": "churn", "task": "classification", "n_train": 100, "metrics": { "AUC": 0.81 } } } }, "WebhookEventModelRefreshed": { "type": "object", "required": [ "id", "type", "created_at", "workspace_id", "data" ], "properties": { "id": { "type": "string", "description": "Stable event identifier. Deduplicate by this id for each receiver." }, "type": { "type": "string", "enum": [ "model.refreshed" ] }, "created_at": { "type": "string", "format": "date-time" }, "workspace_id": { "type": "string" }, "data": { "type": "object", "required": [ "model_id", "version", "outcomes_applied" ], "properties": { "model_id": { "type": "string" }, "version": { "type": "integer" }, "outcomes_applied": { "type": "integer" } } } }, "example": { "id": "evt_example", "type": "model.refreshed", "created_at": "2026-10-11T12:00:00.000Z", "workspace_id": "ws_example", "data": { "model_id": "mdl_example", "version": 2, "outcomes_applied": 12 } } }, "WebhookEventModelStale": { "type": "object", "required": [ "id", "type", "created_at", "workspace_id", "data" ], "properties": { "id": { "type": "string", "description": "Stable event identifier. Deduplicate by this id for each receiver." }, "type": { "type": "string", "enum": [ "model.stale" ] }, "created_at": { "type": "string", "format": "date-time" }, "workspace_id": { "type": "string" }, "data": { "type": "object", "required": [ "model_id", "pending_outcomes", "hint" ], "properties": { "model_id": { "type": "string" }, "pending_outcomes": { "type": "integer" }, "hint": { "type": "string" } } } }, "example": { "id": "evt_example", "type": "model.stale", "created_at": "2026-10-11T12:00:00.000Z", "workspace_id": "ws_example", "data": { "model_id": "mdl_example", "pending_outcomes": 50, "hint": "Refresh the model to integrate these outcomes." } } }, "WebhookEventCreditsLow": { "type": "object", "required": [ "id", "type", "created_at", "workspace_id", "data" ], "properties": { "id": { "type": "string", "description": "Stable event identifier. Deduplicate by this id for each receiver." }, "type": { "type": "string", "enum": [ "credits.low" ] }, "created_at": { "type": "string", "format": "date-time" }, "workspace_id": { "type": "string" }, "data": { "type": "object", "required": [ "credit_balance" ], "properties": { "credit_balance": { "type": "integer" } } } }, "example": { "id": "evt_example", "type": "credits.low", "created_at": "2026-10-11T12:00:00.000Z", "workspace_id": "ws_example", "data": { "credit_balance": 10 } } } } } } ```