# Writing documents

> Put, update and delete documents, in batches, with the durability, visibility and retry rules that apply to every write.

A document is `{"id", "text", "meta", "vector"}`. You write documents to a namespace by id:
writing an id again replaces the whole document. One request can put and delete together, and
it is atomic. This page covers every way to write, and what happens to a write after you send
it.

## Put and delete

`POST /v1/namespaces/{name}/docs` puts (inserts or replaces) documents and deletes ids in one
request: 1 to 10,000 documents and deletes, 64 MB at most.

```sh title="curl"
B=https://api.skimmerdb.com/v1
curl -s $B/namespaces/support/docs -H "Authorization: Bearer $KEY" -d '{
  "docs": [
    {"id": "t-3", "text": "Webhook timeouts since Tuesday", "meta": {"team": "api", "status": "open"}}
  ],
  "deletes": ["t-2"]
}'
# {"written": 1, "deleted": 1, "version": 3, "stored_bytes": 2817, "created": false}
```

```python title="Python"
from skimmerdb import SkimmerDB

db = SkimmerDB()                      # SKIMMERDB_KEY from the environment
ns = db.namespace("support")
r = ns.write(
    [{"id": "t-3", "text": "Webhook timeouts since Tuesday", "meta": {"team": "api", "status": "open"}}],
    deletes=["t-2"],
)
print(r.written, r.deleted)
```

```ts title="TypeScript"
import { SkimmerDB } from "skimmerdb";

const db = new SkimmerDB();            // SKIMMERDB_KEY from the environment
const ns = db.namespace("support");
const r = await ns.write(
  [{ id: "t-3", text: "Webhook timeouts since Tuesday", meta: { team: "api", status: "open" } }],
  { deletes: ["t-2"] },
);
console.log(r.written, r.deleted);
```

- Within one request, deletes apply before puts, so deleting and putting the same id leaves the
  new document.
- `text` may be left out when the document has a vector. `meta` is a JSON object of strings,
  numbers and booleans, up to 16 KB; see [Data model](/guides/data-model) for how to shape it.
- The answer: `written` and `deleted` (counts), `version` (the namespace's version after the
  write), `stored_bytes`, and `created` (`true` when this write made the namespace).

## Update

`PATCH /v1/namespaces/{name}/docs` changes existing documents without resending them whole:
`text` is replaced if given, `meta` is merged key by key, and a `null` value removes a key. A
vector is kept unless the patch gives a new one.

```sh title="curl"
curl -s -X PATCH $B/namespaces/support/docs -H "Authorization: Bearer $KEY" \
  -d '{"docs": [{"id": "t-1", "meta": {"status": "closed", "priority": null}}]}'
```

```python title="Python"
ns.update([{"id": "t-1", "meta": {"status": "closed", "priority": None}}])
```

```ts title="TypeScript"
await ns.update([{ id: "t-1", meta: { status: "closed", priority: null } }]);
```

Updating a namespace that doesn't exist is a 404 (only puts create namespaces).

## Delete

By id, with `POST /v1/namespaces/{name}/delete`:

```sh title="curl"
curl -s $B/namespaces/support/delete -H "Authorization: Bearer $KEY" -d '{"ids": ["t-3"]}'
```

```python title="Python"
ns.delete(["t-3"])
```

```ts title="TypeScript"
await ns.delete(["t-3"]);
```

To delete everything, drop the namespace: `DELETE /v1/namespaces/{name}` (`ns.drop()`). Its
documents are deleted from storage right after.

## Read one document

`GET /v1/namespaces/{name}/docs/{id}` returns the whole document: `id`, `text`, `meta`,
`written_at`, and the vector if it has one. Ids may hold slashes:
`GET .../docs/agent-1/run-42/step-3`.

```sh title="curl"
curl -s $B/namespaces/support/docs/t-1 -H "Authorization: Bearer $KEY"
# {"id": "t-1", "text": "...", "meta": {...}, "written_at": "..."}
```

```python title="Python"
doc = ns.get("t-1")
```

```ts title="TypeScript"
const doc = await ns.get("t-1");
```

## Creating namespaces

The first write to a name that doesn't exist makes the namespace and answers
`"created": true`, so a mistyped name shows. If the documents have vectors, the namespace gets
vectors of the first vector's length, with the cosine metric; otherwise it's text only.

To choose the vector settings yourself (for example the euclidean metric), create it first:

```sh title="curl"
curl -s $B/namespaces -H "Authorization: Bearer $KEY" \
  -d '{"name": "notes", "vector": {"dims": 768, "metric": "euclidean"}}'
```

```python title="Python"
notes = db.create_namespace("notes", dims=768, metric="euclidean", exist_ok=True)
```

```ts title="TypeScript"
const notes = await db.createNamespace("notes", { dims: 768, metric: "euclidean", existOk: true });
```

`dims` (1 to 2,048) and `metric` (`cosine`, the default, or `euclidean`) are fixed for the
namespace's life. See [Vector and hybrid search](/guides/vectors).

## Durability and visibility

- **Durable when answered.** A write is answered once it is stored in object storage. Writes
  from every tenant on a server are logged together every 250 ms, so a write takes about that
  long.
- **Searchable at once on the server that took it**, and on every server within about a
  second.
- **`"wait": true`** answers only once the write is searchable on that server. Use it when the
  next step reads what you just wrote (a test, an agent that writes then searches); leave it
  off otherwise.
- **Atomic.** A request's documents and deletes become visible together or not at all.

More in [Durability](/reference/durability).

## Batching

Many small requests work, since the server groups them every 250 ms anyway, but each one
counts against your rate limit (100 requests a second on free, 1,000 on pro, per server).
A batch of up to 10,000 documents is one request and one atomic entry.

For files of rows (CSV, NDJSON, Parquet), use [import](/guides/import) instead: up to
1,000,000 rows a request, parsed straight into columns.

> **Tip** Append-only data (events, logs, traces written once) is the cheapest shape to store,
> compact and query. Updates are fine, but each one writes a new version and the old one
> waits for compaction.

## Errors and retries

Writes are by id, so resending the same batch is always safe: it replaces what may already be
there.

| Answer | Meaning | What to do |
|---|---|---|
| `429 write_busy` | Writes to this namespace are arriving faster than they commit. Nothing was written. | Wait `Retry-After` seconds (also `retry_after` in the body), resend the same batch. |
| `429 rate_limited` | Over the tenant's or the key's rate, concurrency or cap. | Wait `Retry-After` seconds. |
| `503 unavailable` | Object storage failed. Nothing was written. | Retry. |
| `503 write_unknown` | The write may or may not have been applied. | Retry it: the same ids replace whatever is there. |
| `413 too_large` | Over 10,000 documents or 64 MB. | Split the batch. |
| `422 invalid` | Something in the body is wrong (`fields` says what), e.g. a vector of the wrong length. | Fix the request. |
| `507 quota_full` | The tenant's storage quota is full. | Delete data or upgrade. |

The Python and TypeScript clients retry 429 and 503 answers for you (three times, after
`Retry-After` or backing off), then raise `SkimmerDBError` with `status`, `code` and the
message. All codes: [Errors](/reference/errors).
