{"openapi":"3.1.0","info":{"title":"Lensiq API","description":"Use the Lensiq API to get predictions from your trained models and to send items to Iris.\n\n## Authentication\n\nThere are **three kinds of credentials**. Each one opens a different part of the API, so pick the one that matches the endpoint you're calling.\n\n| Endpoints | Use | Headers |\n| --- | --- | --- |\n| Predict, Models, Jobs | **Account API key** | `X-API-Key` + `X-API-Secret` |\n| Gateway (send items to Iris) | **Iris gateway key** (one per watcher) | `X-API-Key` + `X-API-Secret` |\n| Hooks | **Webhook token** | `X-Webhook-Token` |\n\n> Every secret and token is shown **only once**, when it's created. Store it somewhere safe. If you lose it, create a new key or rotate it.\n\n### Why more than one key?\n\nEach key can only do one job. That keeps you safe:\n\n- **A leaked key can do less harm.** Gateway keys and webhook tokens often live in many places: your backend, scripts, Zapier, and other tools. If one leaks, it can only send items to Iris for that one watcher. It can't read your models or run predictions.\n- **No mix-ups.** Each Iris gateway key belongs to one watcher, so your items always reach the right one, and you don't need to send a watcher id.\n- **Change one without breaking the rest.** Rotating one watcher's Iris gateway key doesn't affect your other watchers or your prediction integration.\n\n### 1. Account API key: Predict, Models, Jobs\n\n**Where to get it:** the **API keys** page in the Lensiq app → *Create key*.\n\n```bash\ncurl https://api.lensiq.app/models \\\n  -H \"X-API-Key: YOUR_ACCOUNT_KEY\" \\\n  -H \"X-API-Secret: YOUR_ACCOUNT_SECRET\"\n```\n\n### 2. Iris gateway key: Gateway\n\n**Where to get it:** in Iris, every watcher gets its own gateway key when it's created. You'll find it on the watcher's page, and *Rotate key* gives you a new one.\n\nIt uses the **same header names** as the account key, but with the Iris gateway key and secret:\n\n```bash\ncurl -X POST https://api.lensiq.app/gateway/events \\\n  -H \"X-API-Key: YOUR_IRIS_GATEWAY_KEY\" \\\n  -H \"X-API-Secret: YOUR_IRIS_GATEWAY_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"data\": {\"order_id\": \"123\", \"amount\": 420}}'\n```\n\n> An account key won't work on Gateway endpoints, and an Iris gateway key won't work anywhere else. You'll get `401` if you mix them up.\n\n### 3. Webhook token: Hooks\n\n**Where to get it:** your watcher → *Inbound* → add an *Inbound webhook* source. You get a URL and a token.\n\nSend the token in the `X-Webhook-Token` header. If your tool can't add headers, add `?token=YOUR_TOKEN` to the URL instead.\n\n```bash\ncurl -X POST \"https://api.lensiq.app/hooks/YOUR_HOOK_KEY\" \\\n  -H \"X-Webhook-Token: YOUR_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"id\": \"123\", \"amount\": 42}'\n```\n\n## Rate limits\n\nLimits are per account and shared by all your API keys. Using the Lensiq app doesn't count toward them. Going over returns **429 Too Many Requests**. Wait and retry.\n\n| Endpoints | Limit |\n| --- | --- |\n| `POST /predict` and `POST /predict/multi` (combined) | 120 calls per minute |\n| `POST /predict/bulk` | 10 calls per hour |\n| Models, Jobs, bulk downloads | 120 calls per minute |\n| Iris gateway pushes and webhooks | 600 calls per minute per gateway key / webhook |\n\nNeed more? Use `/predict/multi` or `/predict/bulk` to send many rows in one call.\n\n## Try it here\n\nClick **Test Request** on any endpoint, pick the right key type under *Authentication*, paste your key and secret, and send a real call.","version":"0.1.0"},"servers":[{"url":"https://api.lensiq.app","description":"Production"}],"paths":{"/jobs/id/{job_id}":{"get":{"tags":["Jobs"],"summary":"Get Job By Id","description":"Check a bulk prediction job, using the `job_id` from `POST /predict/bulk`.\n\n`status`:\n- `queued`, `pending`, `started`: still running.\n- `success`: done. Download with `GET /predict/bulk/download/{job_id}`.\n- `failure`: failed. `error` says why.","operationId":"get_job_by_id_jobs_id__job_id__get","security":[{"AccountKey":[],"AccountSecret":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"integer","title":"Job Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobResponse"}}}},"429":{"description":"Rate limit reached — wait and retry (limits are per account, across all your API keys)"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/models":{"get":{"tags":["Models"],"summary":"List Models","description":"All your trained models, newest first.\n\n- Use a model's `id` as `model_id` in predict calls.\n- `feature_columns` are the input fields that model expects, and `category_options` lists the allowed values for text fields.\n- A model can predict once its `status` is `ready`.","operationId":"list_models_models_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ModelResponse"},"type":"array","title":"Response List Models Models Get"}}}},"429":{"description":"Rate limit reached — wait and retry (limits are per account, across all your API keys)"}},"security":[{"AccountKey":[],"AccountSecret":[]}]}},"/models/{model_id}":{"get":{"tags":["Models"],"summary":"Get Model","description":"Full details of one model: the fields it expects (`feature_columns`, `category_options`), what it predicts (`target_column`, `problem_type`), how accurate it is (`metric_name`, `metric_value`), and which factors matter most (`top_factors`).\n\nReturns **404** if the model doesn't exist or was deleted.","operationId":"get_model_models__model_id__get","security":[{"AccountKey":[],"AccountSecret":[]}],"parameters":[{"name":"model_id","in":"path","required":true,"schema":{"type":"integer","title":"Model Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelResponse"}}}},"429":{"description":"Rate limit reached — wait and retry (limits are per account, across all your API keys)"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/predict":{"post":{"tags":["Prediction"],"summary":"Make Prediction","description":"Get one prediction.\n\n- Put the input fields in `input_data`, using the model's `feature_columns` as keys (see `GET /models/{model_id}`). A missing or invalid field returns **422** with the reason.\n- Uses your **latest model** unless you pass `model_id`.\n- Optional extras (off by default, slower):\n  - `include_explanation`: a plain-English explanation of the result.\n  - `include_shap`: how much each field pushed the prediction up or down.\n\n`probability` is only set for yes/no or category models.","operationId":"make_prediction_predict_post","security":[{"AccountKey":[],"AccountSecret":[]}],"parameters":[{"name":"model_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Model ID to use. Defaults to latest.","title":"Model Id"},"description":"Model ID to use. Defaults to latest."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PredictRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PredictResponse"}}}},"429":{"description":"Rate limit reached — wait and retry (limits are per account, across all your API keys)"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/predict/multi":{"post":{"tags":["Prediction"],"summary":"Make Multi Prediction","description":"Up to **10 predictions in one call**, each with its own `model_id` and `input_data`.\n\n- Items are independent: one failing doesn't stop the others. Check each result's `ok`, and `error` when it failed.\n- `key` is optional. Set it to your own label and it comes back on the matching result.\n- `include_explanation` and `include_shap` work like in `POST /predict` and apply to every item.\n\nCounts as one call for rate limits.","operationId":"make_multi_prediction_predict_multi_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MultiPredictRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MultiPredictResponse"}}}},"429":{"description":"Rate limit reached — wait and retry (limits are per account, across all your API keys)"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"AccountKey":[],"AccountSecret":[]}]}},"/predict/bulk":{"post":{"tags":["Prediction"],"summary":"Bulk Predict","description":"Predict every row of a CSV file in the background.\n\n1. Upload a CSV (`multipart/form-data`, field `file`) with the same columns as your training data. Leave out the column being predicted.\n2. You get back a `job_id` right away (**202**).\n3. Check progress with `GET /jobs/id/{job_id}`. We also email you when it's done.\n4. Download the result with `GET /predict/bulk/download/{job_id}`.\n\nUses your latest model unless you pass `model_id`. `include_explanation` adds an AI explanation per row and makes the job slower.","operationId":"bulk_predict_predict_bulk_post","security":[{"AccountKey":[],"AccountSecret":[]}],"parameters":[{"name":"model_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Model ID to use. Defaults to latest.","title":"Model Id"},"description":"Model ID to use. Defaults to latest."},{"name":"include_shap","in":"query","required":false,"schema":{"type":"boolean","description":"Add a per-row SHAP attribution column to the output CSV.","default":false,"title":"Include Shap"},"description":"Add a per-row SHAP attribution column to the output CSV."},{"name":"include_explanation","in":"query","required":false,"schema":{"type":"boolean","description":"Generate an AI explanation per row (costs LLM tokens).","default":false,"title":"Include Explanation"},"description":"Generate an AI explanation per row (costs LLM tokens)."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_bulk_predict_predict_bulk_post"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkPredictQueuedResponse"}}}},"429":{"description":"Rate limit reached — wait and retry (limits are per account, across all your API keys)"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/predict/bulk/download/{job_id}":{"get":{"tags":["Prediction"],"summary":"Download Bulk Result","description":"Download the finished bulk prediction as a CSV. It has your original columns plus the prediction columns (`predicted_*`, and `ai_explanation` if you asked for it).\n\n- **409**: the job isn't finished yet. Check `GET /jobs/id/{job_id}` and try again.\n- **410**: the result file is no longer available.\n\nThe response body is the CSV file itself. Save it from your code, for example:\n\n```bash\ncurl https://api.lensiq.app/predict/bulk/download/123 \\\n  -H \"X-API-Key: YOUR_ACCOUNT_KEY\" \\\n  -H \"X-API-Secret: YOUR_ACCOUNT_SECRET\" \\\n  -o predictions_123.csv\n```","operationId":"download_bulk_result_predict_bulk_download__job_id__get","security":[{"AccountKey":[],"AccountSecret":[]}],"parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"integer","title":"Job Id"}}],"responses":{"200":{"description":"CSV with original columns plus predicted_* and ai_explanation","content":{"application/json":{"schema":{}},"text/csv":{"schema":{"type":"string","format":"binary"}}}},"429":{"description":"Rate limit reached — wait and retry (limits are per account, across all your API keys)"},"409":{"description":"Job is not complete yet"},"410":{"description":"Result file is no longer available"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/gateway/events":{"post":{"tags":["Gateway"],"summary":"Push Event","description":"Send one item to Iris for the watcher that owns this Iris gateway key. The event is stored and queued right away. The result is sent to your callback when it's ready, or you can poll `GET /gateway/events/{event_id}`.\n\n### What to put in `data`\n\nEach event is one item for Iris to check, so `data` must include:\n\n- **Every field your watcher's model was trained on**, with the **same names** as the columns in your training data and a value for each. Iris needs them to predict.\n- **The item id field** you picked for the watcher (for example `order_id`), so Iris can tell items apart and update the same item over time.\n\nExtra fields are fine and are kept with the event. If a model field or the id is missing or `null`, the call is rejected with **422** (it lists the missing fields) and nothing is stored.\n\n### Safe retries\n\nSend an `Idempotency-Key` header. Sending the same key again returns the original event (`duplicate: true`) instead of creating a new one.","operationId":"push_event_gateway_events_post","security":[{"GatewayKey":[],"GatewaySecret":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Idempotency-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayReceiveRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventAcceptedResponse"}}}},"429":{"description":"Rate limit reached — at most 600 pushes per minute per gateway key"},"422":{"description":"Invalid body, or a model field / the item id field is missing or null (nothing is stored)"}}}},"/gateway/events/{event_id}":{"get":{"tags":["Gateway"],"summary":"Get Event","description":"The status and result of an event you sent with `POST /gateway/events`. Use the same Iris gateway key.\n\n`status`:\n- `received`, `processing`: wait and check again.\n- `done`: the result is filled in.\n- `failed` or `skipped`: `error` says why.\n\nWhen `done`:\n- `prediction`: what the model predicted.\n- `band`: how serious it is. One of `ok`, `watch`, `alert` or `critical`.\n- `recommendation` and `reason`: what to do about it. Empty when `band` is `ok`.\n\nYou usually don't need to poll. The same result is sent to your callback.","operationId":"get_event_gateway_events__event_id__get","security":[{"GatewayKey":[],"GatewaySecret":[]}],"parameters":[{"name":"event_id","in":"path","required":true,"schema":{"type":"integer","title":"Event Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/gateway/alerts/{alert_id}/resolution":{"post":{"tags":["Gateway"],"summary":"Resolve Alert","description":"Tell Iris what you did about an alert. Use the `alert_id` from the alert sent to your callback, and the Iris gateway key of the same watcher.\n\n`resolution`:\n- `actioned`: you acted on it.\n- `false_alarm`: it wasn't a real problem.\n- `ignored`: you chose not to act.\n\nOptionally add `action_taken` and a `note`. No callback is sent for this change.","operationId":"resolve_alert_gateway_alerts__alert_id__resolution_post","security":[{"GatewayKey":[],"GatewaySecret":[]}],"parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertResolutionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/hooks/{hook_key}":{"post":{"tags":["Hooks"],"summary":"Receive Webhook","description":"Send items to Iris from any tool that can POST JSON (Zapier, n8n, Make, Stripe, Shopify, or your own code). Use the URL and token you got when you added the *Inbound webhook* source to your watcher.\n\n### What to send\n\n**There is no fixed shape.** Send the JSON your tool already produces, and Lensiq matches it to your watcher's fields:\n\n- **One JSON object = one item.** To send many items in one call, send a list of objects, or an object with a list inside (set where the list is when you confirm the mapping).\n- Each item is a flat object of fields, matched **by name** (case doesn't matter).\n- Like a gateway event, each item needs **every field your watcher's model was trained on** plus the **item id field**. Fields with different names in your payload can be mapped by hand once, in the app.\n\n```json\n{ \"order_id\": \"123\", \"amount\": 420, \"country\": \"SA\" }\n```\n\n### First call = sample\n\nUntil you confirm the mapping in the app, calls are **only saved as a sample** (`status: \"sample_saved\"`) and nothing is processed. Send one real example, check the matched fields on the watcher's *Inbound* page, then confirm. From then on every call is processed (`status: \"processed\"`).\n\n### Results\n\nThe reply counts what happened: `accepted`, `duplicates`, `rejected` (with `errors` explaining why, for example missing model fields). If nothing in the call could be accepted, you get **422**, so the failure shows up in your tool. Decisions are sent to your callback like any other event.\n\n### Safe retries\n\nSend an `Idempotency-Key` header (or pick an id field in the mapping), and re-sent items won't be counted twice.","operationId":"receive_webhook_hooks__hook_key__post","parameters":[{"name":"hook_key","in":"path","required":true,"schema":{"type":"string","title":"Hook Key"}},{"name":"token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"For senders that can't add headers (Stripe, Shopify)","title":"Token"},"description":"For senders that can't add headers (Stripe, Shopify)"},{"name":"X-Webhook-Token","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Webhook-Token"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookReceiveResponse"}}}},"401":{"description":"Unknown URL, wrong token, or the signature check failed"},"429":{"description":"Rate limit reached — at most 600 calls per minute per webhook"},"422":{"description":"Not JSON, or no item in the call could be accepted (e.g. missing model fields)"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"Any JSON: one object, or a list of objects"},"examples":{"one_item":{"summary":"One item","value":{"order_id":"123","amount":420,"country":"SA"}},"many_items":{"summary":"Many items in one call (set the list path to `orders` when you confirm the mapping)","value":{"orders":[{"order_id":"123","amount":420,"country":"SA"},{"order_id":"124","amount":95,"country":"AE"}]}}}}}}}}},"components":{"schemas":{"AlertResolutionRequest":{"properties":{"resolution":{"type":"string","enum":["actioned","false_alarm","ignored"],"title":"Resolution"},"action_taken":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Action Taken"},"note":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Note"}},"type":"object","required":["resolution"],"title":"AlertResolutionRequest"},"AlertResponse":{"properties":{"id":{"type":"integer","title":"Id"},"watcher_id":{"type":"integer","title":"Watcher Id"},"external_id":{"type":"string","title":"External Id"},"band":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Band"},"is_simulated":{"type":"boolean","title":"Is Simulated"},"status":{"type":"string","title":"Status"},"resolution":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution"},"action_taken":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action Taken"},"resolution_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolution Note"},"resolved_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved By"},"resolved_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Resolved At"},"last_alerted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Alerted At"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"event":{"$ref":"#/components/schemas/EventResponse"}},"type":"object","required":["id","watcher_id","external_id","is_simulated","status","created_at","updated_at","event"],"title":"AlertResponse","description":"One item's current problem. Watcher, item, band and decision come from `event` (the latest meaningful one)."},"Body_bulk_predict_predict_bulk_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_bulk_predict_predict_bulk_post"},"BulkPredictQueuedResponse":{"properties":{"job_id":{"type":"integer","title":"Job Id"},"message":{"type":"string","title":"Message"}},"type":"object","required":["job_id","message"],"title":"BulkPredictQueuedResponse"},"Chart":{"properties":{"type":{"type":"string","title":"Type","description":"How to draw it: \"line\" | \"scatter\" | \"bar\" | \"heatmap\"."},"title":{"type":"string","title":"Title"},"description":{"type":"string","title":"Description","description":"What the chart helps you understand."},"example":{"type":"string","title":"Example","description":"A short plain-words reading using this model's numbers."},"data":{"additionalProperties":true,"type":"object","title":"Data","description":"Points to draw; shape depends on the chart key."}},"type":"object","required":["type","title","description","example","data"],"title":"Chart"},"EventAcceptedResponse":{"properties":{"event_id":{"type":"integer","title":"Event Id"},"watcher_id":{"type":"integer","title":"Watcher Id"},"external_id":{"type":"string","title":"External Id"},"status":{"type":"string","title":"Status"},"duplicate":{"type":"boolean","title":"Duplicate"},"simulated":{"type":"boolean","title":"Simulated"},"notify":{"type":"boolean","title":"Notify"}},"type":"object","required":["event_id","watcher_id","external_id","status","duplicate","simulated","notify"],"title":"EventAcceptedResponse","description":"The push is stored and queued; poll the event for the decision."},"EventResponse":{"properties":{"id":{"type":"integer","title":"Id"},"watcher_id":{"type":"integer","title":"Watcher Id"},"external_id":{"type":"string","title":"External Id"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"status":{"type":"string","title":"Status"},"is_simulated":{"type":"boolean","title":"Is Simulated"},"notify":{"type":"boolean","title":"Notify"},"payload":{"additionalProperties":true,"type":"object","title":"Payload"},"prediction":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Prediction"},"band":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Band"},"threshold":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Threshold"},"answers":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Answers"},"recommendation":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Recommendation"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"decision_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Decision Source"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"attempts":{"type":"integer","title":"Attempts"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"processed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Processed At"}},"type":"object","required":["id","watcher_id","external_id","occurred_at","status","is_simulated","notify","payload","attempts","created_at"],"title":"EventResponse","description":"One event: the input and the engine's decision. Decision fields stay empty until `status` is `done`;\n`threshold`/`recommendation`/`reason` stay empty when `band` is `ok`."},"FeatureImportance":{"properties":{"feature":{"type":"string","title":"Feature"},"importance":{"type":"number","title":"Importance"}},"type":"object","required":["feature","importance"],"title":"FeatureImportance"},"GatewayReceiveRequest":{"properties":{"data":{"additionalProperties":true,"type":"object","title":"Data","description":"Item fields (model features + context fields)."},"occurred_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Occurred At","description":"When the item changed on your side (default: now). Older pushes for the same item are skipped."}},"type":"object","required":["data"],"title":"GatewayReceiveRequest","description":"One pushed item. The gateway key (or the simulate URL) identifies the watcher, so no `watcher_id` is sent.\nSend an `Idempotency-Key` header to make retries safe (default: a hash of `data` + `occurred_at`).","examples":[{"data":{"amount":420,"country":"SA","order_id":"123"},"occurred_at":"2026-10-26T10:00:00Z"}]},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"JobResponse":{"properties":{"id":{"type":"integer","title":"Id"},"task_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Task Id"},"owner_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Id"},"type":{"type":"string","title":"Type"},"status":{"type":"string","title":"Status"},"data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Data"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"parent_job_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Job Id"},"children":{"items":{"$ref":"#/components/schemas/JobResponse"},"type":"array","title":"Children","default":[]},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","task_id","owner_id","type","status","error","created_at","updated_at"],"title":"JobResponse"},"ModelResponse":{"properties":{"id":{"type":"integer","title":"Id"},"version":{"type":"integer","title":"Version"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"dataset_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Dataset Id"},"target_column":{"type":"string","title":"Target Column"},"feature_columns":{"items":{"type":"string"},"type":"array","title":"Feature Columns"},"column_types":{"additionalProperties":{"type":"string"},"type":"object","title":"Column Types"},"category_options":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object","title":"Category Options","description":"Predict dropdown values for label-encoded features with at most 100 unique values. Features over that cap are omitted (free text)."},"problem_type":{"type":"string","title":"Problem Type"},"metric_name":{"type":"string","title":"Metric Name"},"metric_value":{"type":"number","title":"Metric Value"},"algorithm":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Algorithm"},"source":{"type":"string","title":"Source","default":"manual"},"regression_metrics":{"anyOf":[{"$ref":"#/components/schemas/RegressionMetrics"},{"type":"null"}]},"training_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Training Config"},"status":{"type":"string","title":"Status","default":"ready"},"dataset_insights":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Dataset Insights"},"charts":{"additionalProperties":{"$ref":"#/components/schemas/Chart"},"type":"object","title":"Charts","description":"Chart data from the 20% holdout, keyed by chart (roc_curve, pr_curve, calibration, pred_vs_actual, residuals, error_histogram, confusion_matrix, feature_importance)."},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","version","name","dataset_id","target_column","feature_columns","column_types","problem_type","metric_name","metric_value","created_at"],"title":"ModelResponse"},"MultiPredictItem":{"properties":{"key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key"},"model_id":{"type":"integer","title":"Model Id"},"input_data":{"additionalProperties":true,"type":"object","title":"Input Data"}},"type":"object","required":["model_id","input_data"],"title":"MultiPredictItem"},"MultiPredictItemError":{"properties":{"status_code":{"type":"integer","title":"Status Code"},"detail":{"title":"Detail"}},"type":"object","required":["status_code"],"title":"MultiPredictItemError"},"MultiPredictItemResult":{"properties":{"key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key"},"model_id":{"type":"integer","title":"Model Id"},"ok":{"type":"boolean","title":"Ok"},"prediction":{"anyOf":[{"$ref":"#/components/schemas/PredictResponse"},{"type":"null"}]},"error":{"anyOf":[{"$ref":"#/components/schemas/MultiPredictItemError"},{"type":"null"}]}},"type":"object","required":["model_id","ok"],"title":"MultiPredictItemResult"},"MultiPredictRequest":{"properties":{"items":{"items":{"$ref":"#/components/schemas/MultiPredictItem"},"type":"array","maxItems":10,"minItems":1,"title":"Items","description":"Independent predictions (1–10). Not a pipeline — items do not feed each other."},"include_explanation":{"type":"boolean","title":"Include Explanation","default":false},"include_shap":{"type":"boolean","title":"Include Shap","default":false}},"type":"object","required":["items"],"title":"MultiPredictRequest"},"MultiPredictResponse":{"properties":{"results":{"items":{"$ref":"#/components/schemas/MultiPredictItemResult"},"type":"array","title":"Results"}},"type":"object","required":["results"],"title":"MultiPredictResponse"},"PredictRequest":{"properties":{"input_data":{"additionalProperties":true,"type":"object","title":"Input Data"},"include_explanation":{"type":"boolean","title":"Include Explanation","default":false},"include_shap":{"type":"boolean","title":"Include Shap","default":false}},"type":"object","required":["input_data"],"title":"PredictRequest"},"PredictResponse":{"properties":{"prediction":{"title":"Prediction"},"raw_prediction":{"anyOf":[{},{"type":"null"}],"title":"Raw Prediction"},"probability":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Probability"},"probabilities":{"anyOf":[{"additionalProperties":{"type":"number"},"type":"object"},{"type":"null"}],"title":"Probabilities"},"top_factors":{"items":{"$ref":"#/components/schemas/FeatureImportance"},"type":"array","title":"Top Factors"},"prediction_factors":{"anyOf":[{"items":{"$ref":"#/components/schemas/PredictionFactor"},"type":"array"},{"type":"null"}],"title":"Prediction Factors"},"message":{"type":"string","title":"Message"},"explanation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Explanation"},"advanced_explanation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Advanced Explanation"},"steps_context":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Steps Context"},"next_steps":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Next Steps"}},"type":"object","required":["prediction","top_factors","message"],"title":"PredictResponse"},"PredictionFactor":{"properties":{"feature":{"type":"string","title":"Feature"},"contribution":{"type":"number","title":"Contribution"},"direction":{"type":"string","title":"Direction"},"value":{"title":"Value"}},"type":"object","required":["feature","contribution","direction"],"title":"PredictionFactor","description":"Per-prediction SHAP contribution for one feature. Unlike FeatureImportance\n(global, always positive), this is signed and specific to THIS prediction:\n`contribution` > 0 pushed the result up (toward the positive class / a higher\nvalue), < 0 pulled it down. `value` is the input that produced it."},"RegressionMetrics":{"properties":{"mae":{"type":"number","title":"Mae"},"rmse":{"type":"number","title":"Rmse"}},"type":"object","required":["mae","rmse"],"title":"RegressionMetrics"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WebhookReceiveResponse":{"properties":{"status":{"type":"string","enum":["sample_saved","processed"],"title":"Status"},"accepted":{"type":"integer","title":"Accepted","default":0},"duplicates":{"type":"integer","title":"Duplicates","default":0},"removed":{"type":"integer","title":"Removed","default":0},"rejected":{"type":"integer","title":"Rejected","default":0},"event_ids":{"items":{"type":"integer"},"type":"array","title":"Event Ids"},"errors":{"items":{"type":"string"},"type":"array","title":"Errors"}},"type":"object","required":["status"],"title":"WebhookReceiveResponse"}},"securitySchemes":{"AccountKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Your account API key (API keys page in the app). Send together with X-API-Secret."},"AccountSecret":{"type":"apiKey","in":"header","name":"X-API-Secret","description":"The account key's secret, shown once when the key was created."},"GatewayKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Your Iris gateway key (one per watcher) — only works on Gateway endpoints. Send together with X-API-Secret."},"GatewaySecret":{"type":"apiKey","in":"header","name":"X-API-Secret","description":"The Iris gateway secret, shown once when the watcher was created or its key rotated."}}}}