POST /v1/clean
POST https://api.sequenxai.com/v1/clean is the free cleaning dry run (Beta). It runs the same cleaning stage as /v1/forecast and returns only the cleaning report and the effective policy: no model call, no forecast. Use it to confirm how your data will be treated before forecasting, and to collect the decision ids you can pass to accept / decline.
1. Request examples
# 12 hourly slots; 06:00 is missing. Free dry run: no model call, no points billed.
curl -sS -X POST "https://api.sequenxai.com/v1/clean" \
-H "Authorization: Bearer $SEQUENX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"freq": "1h",
"series": [
{"unique_id": "meter-001", "ds": "2026-09-01T00:00:00Z", "y": 142.5},
{"unique_id": "meter-001", "ds": "2026-09-01T01:00:00Z", "y": 141.8},
{"unique_id": "meter-001", "ds": "2026-09-01T02:00:00Z", "y": 140.9},
{"unique_id": "meter-001", "ds": "2026-09-01T03:00:00Z", "y": 139.7},
{"unique_id": "meter-001", "ds": "2026-09-01T04:00:00Z", "y": 139.2},
{"unique_id": "meter-001", "ds": "2026-09-01T05:00:00Z", "y": 138.8},
{"unique_id": "meter-001", "ds": "2026-09-01T07:00:00Z", "y": 138.2},
{"unique_id": "meter-001", "ds": "2026-09-01T08:00:00Z", "y": 139.0},
{"unique_id": "meter-001", "ds": "2026-09-01T09:00:00Z", "y": 140.4},
{"unique_id": "meter-001", "ds": "2026-09-01T10:00:00Z", "y": 141.3},
{"unique_id": "meter-001", "ds": "2026-09-01T11:00:00Z", "y": 142.0}
]
}'import math
import os
import requests
API_URL = "https://api.sequenxai.com/v1/clean"
HEADERS = {"Authorization": f"Bearer {os.environ['SEQUENX_API_KEY']}"}
# 48 hourly slots: 2026-09-01T20:00 is missing and one reading is the logger code -9999.
series = []
for i in range(48):
if i == 20:
continue
y = round(140 + 8 * math.sin(2 * math.pi * i / 24), 1)
series.append({
"unique_id": "meter-001",
"ds": f"2026-09-{1 + i // 24:02d}T{i % 24:02d}:00:00Z",
"y": -9999 if i == 30 else y,
})
def dry_run(options=None):
payload = {"series": series, "freq": "1h", "model": "hora-1.0"}
if options:
payload["options"] = options
resp = requests.post(API_URL, headers=HEADERS, json=payload, timeout=60)
body = resp.json()
if resp.status_code != 200:
error = body["error"]
raise SystemExit(f"{resp.status_code} {error['code']}: {error['message']} ({error['request_id']})")
return body
report = dry_run()["cleaning_report"]
for decision in report["decisions"]:
print(decision["id"], decision["class"], decision["status"])
# value.sentinel:meter-001:-9999 fabricate applied
# grid.regrid:meter-001 structural applied
# gap.impute:meter-001 fabricate applied
# -9999 is a real reading here: decline the decision by its id and look again.
report = dry_run({"cleaning": {"decline": ["value.sentinel:meter-001:-9999"]}})["cleaning_report"]
print([(d["id"], d["status"]) for d in report["decisions"]][0])
# ('value.sentinel:meter-001:-9999', 'declined')// Node.js 18+. Same 12-slot series as the curl example.
const hours = [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11];
const values = [142.5, 141.8, 140.9, 139.7, 139.2, 138.8, 138.2, 139.0, 140.4, 141.3, 142.0];
const series = hours.map((h, i) => ({
unique_id: "meter-001",
ds: `2026-09-01T${String(h).padStart(2, "0")}:00:00Z`,
y: values[i],
}));
const resp = await fetch("https://api.sequenxai.com/v1/clean", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEQUENX_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ series, freq: "1h" }),
});
const body = await resp.json();
if (!resp.ok) throw new Error(`${resp.status} ${body.error.code}: ${body.error.message}`);
console.log(body.effective_policy.id);
for (const d of body.cleaning_report.decisions) {
console.log(d.id, d.class, d.status, d.before, d.after);
}2. Request body
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
series | array | Yes | — | Long-table rows as in /v1/forecast, at least 1 and at most 200,000 per request. |
freq | string | No | Inferred | As in /v1/forecast. |
clean_series | boolean | No | true | false detects without repairing and returns 422 as soon as a repair is needed. |
model | string | No | — | When given, that model's minimum length is checked (SERIES_TOO_SHORT) and an entity longer than its maximum context returns 422 SERIES_TOO_LONG, as /v1/forecast would; history is never cut. Same values as /v1/forecast. |
options | object | No | — | Same as /v1/forecast: timezone, ds_format, gap_split_min_steps, cleaning.preset / version / accept / decline, and the 2026-11 knobs; unknown fields inside options.cleaning return 422 VALIDATION_ERROR. Use it to preview policy version 2026-11 for free. |
Forecast fields such as h, level or future_covariates do not exist here; sending them returns 422 VALIDATION_ERROR because they are outside the contract.
3. Limits and billing
| Item | Rule |
|---|---|
| Billing | Free. Each call is recorded as a usage row with zero billable points. |
| Daily point quota | Not consumed. |
| Rate limit | 60 calls per minute per account (sliding window shared by all its keys), counted separately from /v1/forecast; above that, 429 with X-RateLimit-Policy: clean-rpm. |
| Shared capacity | Cleaning capacity is shared by all accounts and dry runs never queue: while it is full, 503 SERVICE_UNAVAILABLE with Retry-After; retry after that interval. |
| Rows per request | At most 200,000, otherwise 422 POINTS_EXCEEDED (an oversized call is refused, never queued); the same code is returned when the regular grid implied by the series is too large. |
raw preset | Not accepted: raw skips cleaning, so there is no report to show; returns 422 CLEANING_OPTION_UNSUPPORTED (details.field is options.cleaning.preset). |
4. Response fields
| Field | Notes |
|---|---|
request_id | Server request ID, same as the X-Request-Id header. |
freq | Your freq as sent; when omitted, the inferred canonical token. |
cleaning_report | The cleaning_report/1 structured report, described in Cleaning Presets & Report. |
effective_policy | {id, preset, version, source}, as in /v1/forecast. |
5. Response example
The response to the curl request in section 1, with the series[] detail abridged:
{
"request_id": "req_9c2e7b1d4a8f4f3e9d6c5b4a3f2e1d0c",
"freq": "1h",
"cleaning_report": {
"schema_version": "cleaning_report/1",
"policy": {
"id": "default@2026-13",
"clean_series": true,
"duplicate_policy": "last",
"gap_fill_max_steps": 3,
"gap_split_min_steps": null,
"timezone_default": "UTC",
"accept": [],
"decline": [],
"...": "..."
},
"overall": {
"series_count": 1,
"input_points": 11,
"output_points": 12,
"health_score": 84,
"health_tier": "Healthy",
"forecastable": true
},
"series": ["... per-series input / freq / issues / actions / metrics / timezone ..."],
"warnings": [],
"blocking": [],
"decisions": [
{
"id": "grid.regrid:meter-001",
"rule": "grid.regrid",
"class": "structural",
"status": "applied",
"unique_id": "meter-001",
"action": "insert_empty_slots",
"before": {"slots": 11},
"after": {"slots": 12},
"reversible": false,
"evidence": {}
},
{
"id": "gap.impute:meter-001",
"rule": "gap.impute",
"class": "fabricate",
"status": "applied",
"unique_id": "meter-001",
"action": "pchip",
"before": {"missing": 1},
"after": {"filled": 1},
"reversible": false,
"evidence": {}
}
]
},
"effective_policy": {
"id": "default@2026-13",
"preset": "default",
"version": "2026-13",
"source": "platform"
}
}6. When cleaning blocks
When the data cannot be forecast safely under the current policy, /v1/clean returns the same 422 code as /v1/forecast would, with the full report in error.details.cleaning_report. blocking[] says why:
{
"error": {
"code": "SERIES_DISCONTINUOUS",
"message": "A gap ending shortly before the last observation is too long to fill reliably; the forecast would start from filled values. Send the missing recent values, or send only the history before the gap",
"request_id": "req_…",
"details": {
"cleaning_report": {
"schema_version": "cleaning_report/1",
"overall": {"forecastable": false, "...": "..."},
"blocking": [
{"code": "health_critical", "unique_id": "meter-001", "count": null, "missing_ratio": 0.119048,
"reason": "gap_near_origin_too_long", "reasons": ["gap_near_origin_too_long"],
"period": 24, "near_gap_steps": 20, "far_gap_steps": 0}
],
"decisions": ["..."]
}
}
}
}How to resolve each code is in Error Codes.
7. How it relates to /v1/forecast
- With the same
series,freq,modelandoptions, both produce the same cleaning result and decisionids, so ids seen in the dry run can go straight intooptions.cleaning.accept/declineon/v1/forecast. - Without
model, the dry run checks neither the minimum length nor the maximum context; pass the samemodelto see exactly the report (or theSERIES_TOO_LONGrejection) the forecast will produce. - The forecast additionally checks
hand the daily quota; those are outside the dry run.