Data Cleaner: Zero-touch data cleaning

> Turn any messy data or URL into a clean, typed table. Use it before analysing a CSV, Excel, JSON or XML file. Every column is typed, every cell is normalised, and each cell carries a status, so nothing is changed silently.

Site: https://data-cleaner.com/ API base: https://data-cleaner.com/clean/v1 OpenAPI: https://data-cleaner.com/clean/v1/openapi.json Swagger UI: https://data-cleaner.com/clean/v1/docs

What it does

Data Cleaner takes a messy table and returns a clean one. It infers the type of every column, such as date, number, currency, address or free text, and rewrites each cell in a canonical form, keeping a status per cell so that nothing is changed silently. The same engine answers over a REST API, one value or one column at a time, so a script, a spreadsheet or an agent can use it without the browser.

Two ways to use it

In the browser

Drag a CSV or Excel file onto the page. The columns are typed, the cells are cleaned, and the result is shown as a table you can inspect, correct and download.

Over the REST API

Every endpoint lives under https://data-cleaner.com/clean/v1. Send a value or a column, get JSON back. No key is needed to start; a key lifts the per-request limit and unlocks the datetime endpoints. The OpenAPI document at https://data-cleaner.com/clean/v1/openapi.json is the reference.

Tiers

Limits

Getting a key

Keys are issued by hand. Write to egaere (at) ethz.ch with a line on what you want to clean.

About the API (SerenTec Cleaning API 0.2.0)

Clean a value, a column or a datetime string, or infer the type of a value or an array. Every endpoint lives under /clean/v1. Every call is proxied to the cleaning fleet; this service holds no database and no session state.

Authentication. A key is optional for /column, /value, /type and /types and required for /datetime. Send it in the X-API-Key header. The api_key query parameter exists for Excel WEBSERVICE(), which cannot set headers; a key in a URL lands in server logs and browser history, so prefer the header everywhere else. A wrong key is served as anonymous, not refused.

Tiers. An anonymous caller is served under a per-request cap; a valid key lifts it. A request above its cap is refused, never truncated: 401 for an anonymous caller (a key would lift the cap), 413 for a keyed caller. GET /clean/v1/info reports both caps and the accepted entity hints.

Browsers. CORS allows any origin, methods GET and POST, so a page or a browser-side agent may call this API directly.

Accepted values of the entity hint, matched case-insensitively: BINARY, BOOK_NAME, COUNTRY, CREDIT_CARD, CURRENCY, DATE, DATETIME, FLOAT, GEOMETRY_GEOJSON, GEOMETRY_WKB, HEX_ID, IBAN, INTEGER, ISBN, JOB, JSON, LANGUAGE, NULL, NUMBER, PERSON_NAME, PLACE, PYTHON_OBJECT, QUANTITY, STRING, SWIFT, TEXT, TIME, TSID, URL, UUID, XML, YEAR, YEAR_MONTH, YEAR_QUARTER, YEAR_WEEK, YEAR_WEEK_DAY.

Hints with a dedicated cleaner: CURRENCY, DATE, DATETIME, FLOAT, INTEGER, NUMBER, PERSON_NAME, PLACE, QUANTITY, TIME, YEAR, YEAR_MONTH, YEAR_QUARTER, YEAR_WEEK, YEAR_WEEK_DAY. A hint outside this set types the values and returns them as text.

Endpoints

MethodPathWhat it doesKey
POSThttps://data-cleaner.com/clean/v1/columnClean a column of valuesoptional
GEThttps://data-cleaner.com/clean/v1/valueClean one valueoptional
GEThttps://data-cleaner.com/clean/v1/typeInfer the type of one value without cleaning itoptional
POSThttps://data-cleaner.com/clean/v1/typesInfer the common type of an array of values without cleaning themoptional
GEThttps://data-cleaner.com/clean/v1/datetimeClean one datetime stringrequired
POSThttps://data-cleaner.com/clean/v1/datetimeClean a column of datetime stringsrequired
GEThttps://data-cleaner.com/clean/v1/healthLiveness: this process is upnone
GEThttps://data-cleaner.com/clean/v1/infoThe two caps, the accepted entity hints and the auth schemenone

Status codes

A wrong key is served as anonymous, not refused; only the key-required endpoints answer 401 to a missing key.

na_code legend

Every cleaned cell carries na_code: 0 valid, 1 missing, 2 invalid, 3 error, 4 warning.

Endpoint reference

POST /column

POST https://data-cleaner.com/clean/v1/column (key: optional)

Clean a column of values. The column's type is inferred from the values, or pinned by the entity hint; every value comes back as a CleanedCell in the input order. A hint the fleet refuses is reported as text with the reason in each cell's message and na_code 4 (warning), so an untouched value is never mistaken for a cleaned one.

Query parameters:

NameTypeRequiredDescription
api_keystring or nullnoAPI key; prefer the X-API-Key header

Headers: X-API-Key (the API key)

Request body (JSON):

CleanRequest: Request to clean a column of values (POST /column). Sent from Excel via VBA, curl or Python.

FieldTypeRequiredDefaultDescription
api_keystring or nullnoAPI key for clients that cannot set headers; the X-API-Key header is preferred
column_namestringno"column"A label for the column; appears in logs only
entitystring or nullnoEntity type hint, case-insensitive (e.g. "date", "currency", "number"); omit to auto-detect. GET /info lists the accepted values.
valuesarray of stringyesThe values to clean, one string per cell

Response 200 (JSON):

CleanResult: Full cleaning result for a column.

FieldTypeRequiredDefaultDescription
entity_confidencenumber or nullnoInference confidence in [0, 1]; null when a hint skipped inference
entity_typestring or nullnoThe entity type the column was cleaned as: inferred, or the hint when it was honoured
resultsarray of CleanedCellyesOne cell per input value, same order

Other status codes:

curl:


curl -X POST https://data-cleaner.com/clean/v1/column -H "Content-Type: application/json" \
     -d '{"values": ["1.1.2024", "2024-02-01", "Feb 3, 2024"], "entity": "date"}'

Python:


import requests

r = requests.post("https://data-cleaner.com/clean/v1/column", json={"values": ["1.1.2024", "2024-02-01", "Feb 3, 2024"], "entity": "date"})
print(r.status_code, r.json())

GET /value

GET https://data-cleaner.com/clean/v1/value (key: optional)

Clean one value. The type is inferred from the value alone, or pinned by the entity hint. Always JSON, so a VBA or Excel client can convert by type. A fleet fault is reported in-band as na_code 3 with the message, not as a 5xx, so a spreadsheet cell shows the reason rather than an error.

Query parameters:

NameTypeRequiredDescription
valuestringyesValue to clean
entitystring or nullnoEntity type hint, case-insensitive (e.g. "date", "currency", "number"); omit to auto-detect. GET /info lists the accepted values.
formatstring or nullnoReserved; ignored
api_keystring or nullnoAPI key; prefer the X-API-Key header

Headers: X-API-Key (the API key)

Response 200 (JSON):

CleanedValue: One cleaned value (GET /value, GET /datetime). Always JSON: VBA needs the type to convert.

FieldTypeRequiredDefaultDescription
cleanstringyesThe cleaned value; the input unchanged when nothing was cleaned
confidencenumber or nullnoParser confidence in [0, 1], when available
is_ambiguousbooleanyesTrue when more than one reading is plausible
messagestring or nullnoHuman-readable status
na_codeintegeryesStatus code: 0 valid, 1 missing, 2 invalid, 3 error, 4 warning
typestringyesThe entity type inferred, or the hint when honoured; "" if unknown

Other status codes:

curl:


curl -G https://data-cleaner.com/clean/v1/value --data-urlencode "value=1.1.2024"

Python:


import requests

r = requests.get("https://data-cleaner.com/clean/v1/value", params={"value": "1.1.2024"})
print(r.status_code, r.json())

GET /type

GET https://data-cleaner.com/clean/v1/type (key: optional)

Infer the type of one value without cleaning it. The inference block carries the predicted class, its probability and the alternatives considered. A fleet fault is reported in-band as na_code 3 with the message, not as a 5xx.

Query parameters:

NameTypeRequiredDescription
valuestringyesValue for which type must be inferred
formatstring or nullnoReserved; ignored
api_keystring or nullnoAPI key; prefer the X-API-Key header

Headers: X-API-Key (the API key)

Response 200 (JSON):

TypeResult: Type inference result (GET /type, POST /types).

FieldTypeRequiredDefaultDescription
inferenceobjectyesThe full inference block: predicted_class, class_p and the alternatives considered
typestringyesThe inferred entity type; "" when none was inferred

Other status codes:

curl:


curl -G https://data-cleaner.com/clean/v1/type --data-urlencode "value=2024-02-01"

Python:


import requests

r = requests.get("https://data-cleaner.com/clean/v1/type", params={"value": "2024-02-01"})
print(r.status_code, r.json())

POST /types

POST https://data-cleaner.com/clean/v1/types (key: optional)

Infer the common type of an array of values without cleaning them. One verdict for the whole array, the way a column is typed. A fleet fault is reported in-band as na_code 3 with the message, not as a 5xx.

Query parameters:

NameTypeRequiredDescription
api_keystring or nullnoAPI key; prefer the X-API-Key header

Headers: X-API-Key (the API key)

Request body (JSON):

TypeArrayRequest: Request to infer the type of an array of values (POST /types).

FieldTypeRequiredDefaultDescription
api_keystring or nullnoAPI key for clients that cannot set headers; the X-API-Key header is preferred
column_idstring or nullno
column_namestringno"range"A label for the array; appears in logs only
dataarray of stringyesThe values whose common type is to be inferred
formatstring or nullnoReserved; ignored
job_uuidstring or nullno
page_numintegerno0
session_uuidstring or nullno
skip_cleaningbooleannofalse

Response 200 (JSON):

TypeResult: Type inference result (GET /type, POST /types).

FieldTypeRequiredDefaultDescription
inferenceobjectyesThe full inference block: predicted_class, class_p and the alternatives considered
typestringyesThe inferred entity type; "" when none was inferred

Other status codes:

curl:


curl -X POST https://data-cleaner.com/clean/v1/types -H "Content-Type: application/json" \
     -d '{"data": ["CH", "DE", "FR"]}'

Python:


import requests

r = requests.post("https://data-cleaner.com/clean/v1/types", json={"data": ["CH", "DE", "FR"]})
print(r.status_code, r.json())

GET /datetime

GET https://data-cleaner.com/clean/v1/datetime (key: required)

Clean one datetime string. Same answer shape as GET /value with entity=datetime, but a valid API key is required.

Query parameters:

NameTypeRequiredDescription
valuestringyesDatetime string to clean
api_keystring or nullnoAPI key; prefer the X-API-Key header

Headers: X-API-Key (the API key)

Response 200 (JSON):

CleanedValue: One cleaned value (GET /value, GET /datetime). Always JSON: VBA needs the type to convert.

FieldTypeRequiredDefaultDescription
cleanstringyesThe cleaned value; the input unchanged when nothing was cleaned
confidencenumber or nullnoParser confidence in [0, 1], when available
is_ambiguousbooleanyesTrue when more than one reading is plausible
messagestring or nullnoHuman-readable status
na_codeintegeryesStatus code: 0 valid, 1 missing, 2 invalid, 3 error, 4 warning
typestringyesThe entity type inferred, or the hint when honoured; "" if unknown

Other status codes:

curl:


curl -G https://data-cleaner.com/clean/v1/datetime -H "X-API-Key: YOUR_API_KEY" --data-urlencode "value=19:34, Tuesday 31 March 2026"

Python:


import requests

r = requests.get("https://data-cleaner.com/clean/v1/datetime", params={"value": "19:34, Tuesday 31 March 2026"}, headers={"X-API-Key": "YOUR_API_KEY"})
print(r.status_code, r.json())

POST /datetime

POST https://data-cleaner.com/clean/v1/datetime (key: required)

Clean a column of datetime strings. Same answer shape as POST /column with entity=datetime, but a valid API key is required. The body is a bare JSON array, so the key travels in the X-API-Key header or the api_key query parameter.

Query parameters:

NameTypeRequiredDescription
api_keystring or nullnoAPI key; prefer the X-API-Key header

Headers: X-API-Key (the API key)

Request body (JSON):

JSON array of string.

Response 200 (JSON):

CleanResult: Full cleaning result for a column.

FieldTypeRequiredDefaultDescription
entity_confidencenumber or nullnoInference confidence in [0, 1]; null when a hint skipped inference
entity_typestring or nullnoThe entity type the column was cleaned as: inferred, or the hint when it was honoured
resultsarray of CleanedCellyesOne cell per input value, same order

Other status codes:

curl:


curl -X POST https://data-cleaner.com/clean/v1/datetime -H "Content-Type: application/json" -H "X-API-Key: YOUR_API_KEY" \
     -d '["19:34, Tuesday 31 March 2026", "2024-02-01 08:00"]'

Python:


import requests

r = requests.post("https://data-cleaner.com/clean/v1/datetime", json=["19:34, Tuesday 31 March 2026", "2024-02-01 08:00"], headers={"X-API-Key": "YOUR_API_KEY"})
print(r.status_code, r.json())

GET /health

GET https://data-cleaner.com/clean/v1/health (key: none)

Liveness: this process is up. Says nothing about the fleet behind it.

Response 200 (JSON):

HealthResult: Liveness (GET /health): this process answers. Says nothing about the fleet behind it.

FieldTypeRequiredDefaultDescription
prefixstringyesThe URL prefix every endpoint lives under
servicestringyesService name
statusstringyes"ok" when this process is up

curl:


curl https://data-cleaner.com/clean/v1/health

Python:


import requests

r = requests.get("https://data-cleaner.com/clean/v1/health")
print(r.status_code, r.json())

GET /info

GET https://data-cleaner.com/clean/v1/info (key: none)

The two caps, the accepted entity hints and the auth scheme. Read it before the first request. No key needed; no addresses, no key material. The entity hints are read from the fleet (CleanColumn's parser registry) on every call, so a parser added to the fleet appears here without a change to this service.

Response 200 (JSON):

InfoResult: What a caller needs before the first request (GET /info): caps, hints, the auth scheme.

FieldTypeRequiredDefaultDescription
anonymous_max_valuesintegeryesValues per request without a key; 0 means a key is mandatory
authobjectyesHow a key travels: header (preferred) and query parameter
cleaning_entitiesarray of stringyesThe subset with a dedicated cleaner; a hint outside it types the values and returns them as text
corsobjectyesBrowser policy: any origin may call, GET and POST
entitiesarray of stringyesAccepted values of the entity hint, as the fleet registers them; matched case-insensitively
key_requiredarray of stringyesPaths under the prefix that refuse an anonymous caller (401)
keyed_max_valuesintegeryesValues per request with a valid key
openapistringyesPath of the OpenAPI document under the prefix
prefixstringyesThe URL prefix every endpoint lives under
servicestringyesService name
versionstringyesService version

Other status codes:

curl:


curl https://data-cleaner.com/clean/v1/info

Python:


import requests

r = requests.get("https://data-cleaner.com/clean/v1/info")
print(r.status_code, r.json())

Excel

WEBSERVICE() cannot set headers, so the key travels in the query string; a key in a URL lands in server logs and browser history.


=WEBSERVICE("https://data-cleaner.com/clean/v1/value?value=" & ENCODEURL(A1))
=WEBSERVICE("https://data-cleaner.com/clean/v1/datetime?value=" & ENCODEURL(A1) & "&api_key=YOUR_API_KEY")

Data handling

Research

data-cleaner is the state of the art in real-time data-cleaning technologies.

data-cleaner is built and maintained by Edward Gaere at ETH Zurich. The research behind it, with Prof. Dr. Florian von Wangenheim, creates new benchmarks and real-time cleaning models for real-world data as it actually appears, not after it has been tidied. The benchmarks measure how leading machine-learning and language models handle that data. data-cleaner is designed to handle the majority of surface forms arising in practice: bytes, encodings, file formats and their versions, dialects, morphology, locales, aliases, component permutations, and so on.

The MESSY series

Benchmarks on real-world data: addresses, datetimes and files.


Addresses: MESSY STREETS

MESSY STREETS: A Benchmark for Geocoding Real-World Addresses. Edward Gaere and Florian von Wangenheim. Accepted as a short paper at ACM SIGSPATIAL 2026, Riverside, 3 to 6 November 2026.

A benchmark of addresses taken verbatim from the web, with parts that are missing, repeated or malformed, and with reference locations from OpenAddresses and OpenStreetMap. Twelve commercial and open-source geocoders are compared on it. Commercial geocoders find far more of these addresses, and most of the gap comes from addresses that are not written in canonical form. That is the case for cleaning an address before geocoding it.


Datetimes: MESSY TIMES

PRIMETIME: Limits of LLMs in Temporal Primitives. Edward Gaere and Florian von Wangenheim. The preprint of MESSY TIMES.

Language models are tested on each basic datetime operation in isolation. The operations are individually unreliable across models, and learnable by fine-tuning on generated data.


Files: MESSY CSV

Forthcoming.

Contact

Edward Gaere, ETH Zurich. egaere (at) ethz.ch