# Importing files

> Load CSV, NDJSON or Parquet into a namespace in one request, parsed straight into typed columns, every row visible together.

For data that already lives in files (exports, logs, tables from a warehouse or a lake), import
it in one request instead of writing documents in batches. Every row becomes a document; the
file is parsed straight into typed columns, and all rows become visible together, or none.

`POST /v1/namespaces/{name}/import?format=csv|ndjson|parquet&id=<column>&text=<col,col>` with
the file as the body. Write scope; up to 2 GB and 1,000,000 rows a request.

```sh title="curl"
B=https://api.skimmerdb.com/v1
curl -s "$B/namespaces/support/import?format=csv&id=ticket&text=subject,body" \
  -H "Authorization: Bearer $KEY" --data-binary @tickets.csv
```

```python title="Python"
import os, requests

with open("tickets.csv", "rb") as f:
    r = requests.post(
        "https://api.skimmerdb.com/v1/namespaces/support/import",
        params={"format": "csv", "id": "ticket", "text": "subject,body"},
        headers={"Authorization": f"Bearer {os.environ['SKIMMERDB_KEY']}"},
        data=f,
    )
print(r.json())
```

```ts title="TypeScript"
import { readFile } from "node:fs/promises";

const url = "https://api.skimmerdb.com/v1/namespaces/support/import?format=csv&id=ticket&text=subject,body";
const res = await fetch(url, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SKIMMERDB_KEY}` },
  body: await readFile("tickets.csv"),
});
console.log(await res.json());
```

The answer:

```json
{"written": 52000, "deleted": 0, "version": 4, "stored_bytes": 18412203, "rows": 52000, "took_ms": 1840, "created": false}
```

An import to a name that doesn't exist makes the namespace (text only; `"created": true`).

## Parameters

| Parameter | Meaning |
|---|---|
| `format` | `csv` (the default), `ndjson` or `parquet`. |
| `id` | The column whose values are the documents' ids. Without it, ids are made from the import and the row number. A later row with the same id wins. |
| `text` | The columns that make each document's searchable text, as `name: value` lines (`note: refund requested`). None: rows are found by SQL and filters only. |
| `schema` | CSV only: a `CREATE TABLE` statement (URL-encoded) giving the columns and their types, for files without a header row. |

Every column is metadata, whether or not it's in `text`: filters, SQL and search hits all see
it.

> **Tip** Pick a natural key for `id` (an order number, a ticket number) and re-importing the
> same file replaces rather than duplicates. See [Data model](/guides/data-model#ids).

## Types

| Format | How columns are typed |
|---|---|
| CSV | A header row is required (or `schema=`). Kinds are inferred: integers and `true`/`false` are `int`, other numbers `float`, `YYYY-MM-DD` a `date`, `YYYY-MM-DD HH:MM:SS` a `time`, anything else `text`. |
| NDJSON | One JSON object a line, its keys the columns, typed like `meta` (integers and booleans `int`, other numbers `float`, strings `text`). |
| Parquet | Keeps its types. Lists and maps aren't supported yet. |

`date` and `time` columns compare with strings like `'2026-10-01'` in SQL and work with
`DATE_TRUNC`. See [Analytics with SQL](/guides/analytics#from-files).

### NULL versus empty

NDJSON and Parquet keep `NULL` separate from empty strings. In CSV, an empty field (quoted or
not) means a missing value. Use NDJSON or Parquet when the difference between `NULL` and empty
text matters.

## NDJSON

One flat object a line:

```json title="events.ndjson"
{"event_id": "e-1", "ts": 1760572937, "agent": "agent-1", "tool": "search", "ok": false, "message": "error E504 upstream timeout"}
{"event_id": "e-2", "ts": 1760572951, "agent": "agent-1", "tool": "sql_query", "ok": true, "message": "ok"}
```

Import it:

```sh title="curl"
curl -s "$B/namespaces/events/import?format=ndjson&id=event_id&text=message" \
  -H "Authorization: Bearer $KEY" --data-binary @events.ndjson
```

## CSV without a header

Give the columns with `schema=`, a URL-encoded `CREATE TABLE`:

```sh title="curl"
SCHEMA=$(jq -rn --arg s "CREATE TABLE orders (order_id TEXT, placed_at TIMESTAMP, country TEXT, amount DOUBLE)" '$s|@uri')
curl -s "$B/namespaces/orders/import?format=csv&id=order_id&schema=$SCHEMA" \
  -H "Authorization: Bearer $KEY" --data-binary @orders-noheader.csv
```

Types: `TEXT` (or `VARCHAR`, `STRING`), `DOUBLE` (or `FLOAT`, `REAL`), `DATE`, `TIMESTAMP`
(or `DATETIME`), and integer types (`INT`, `BIGINT`, ...).

## Speed and limits

Measured on 8 vCPUs: CSV about 440,000 rows a second, Parquet about 500,000, NDJSON about
175,000. A request takes up to 2 GB and 1,000,000 rows; split bigger files into several
imports. Each import is one atomic write: its rows become visible together, after which they
compact with the rest of the namespace like any write.

An import is metered as a write of that many documents and the file's bytes. See
[Limits and plans](/reference/limits).
