# KV

> Get, set and delete keys with TTLs and versions; batches and atomic increments.

Source: https://useprimitives.com/docs/api/kv

These routes apply to a primitive of type `kv`. See [KV](/docs/primitives/kv) for the concepts. Calling one of them on a primitive of another type answers `400 wrong_type`.

| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/v1/primitives/{id}/keys` | List keys |
| `GET` | `/v1/primitives/{id}/keys/{key}` | Get a key |
| `PUT` | `/v1/primitives/{id}/keys/{key}` | Set a key |
| `DELETE` | `/v1/primitives/{id}/keys/{key}` | Delete a key |
| `POST` | `/v1/primitives/{id}/mget` | Get many keys |
| `POST` | `/v1/primitives/{id}/mset` | Set many keys |
| `POST` | `/v1/primitives/{id}/mdelete` | Delete many keys |
| `POST` | `/v1/primitives/{id}/incr` | Increment a counter |

`{key}` is the rest of the path, so keys may contain `/`. Percent-encode characters that aren't safe in a URL path. Keys are 1–1,024 bytes of UTF-8, with no control characters, no leading `/` and no `.` or `..` segment; any other key is `400 invalid_request`.

Every route answers `423 paused` while the primitive is paused. Timestamps are milliseconds since the Unix epoch. See [Errors](/docs/api/errors) for the full list of codes.

An expired key is treated as absent everywhere: reads and lists skip it, and writes see it as missing.

## Versions and `if_version`

Every write gives the key a new `version`. Versions come from one counter per store that only goes up, so they aren't consecutive per key, and a key that is deleted and set again never gets an old version back.

Writes that take `if_version` only happen if the key is at that version now:

- `if_version: 7` writes only if the key's current version is `7`.
- `if_version: 0` writes only if the key doesn't exist (or has expired). Use it to create a key once, such as a lock.
- Leaving it out writes unconditionally.

A mismatch answers `412 precondition_failed` and writes nothing. The message says which version the key is at, or that it doesn't exist. A negative `if_version` is `400 invalid_request`.

## List keys

`GET /v1/primitives/{id}/keys`

Lists live keys in byte order, optionally under a prefix. Values are left out unless `values` is set: without it, entries have no `value` field at all. Pages hold 100 keys by default and up to 1,000; see [Pagination](/docs/api#pagination).

Key tool: `kv_list`.

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `prefix` | string | Only keys that start with this. |
| `cursor` | string | The `next_cursor` of the previous page. Opaque. |
| `limit` | integer | Page size. Default `100`, max `1000`. |
| `values` | flag | Include each key's `value`. A flag is on for `1`, `true`, `yes` or an empty value. Default off. |

```bash
curl "https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/keys?prefix=user:42:&limit=2" \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

```json
{
  "data": [
    {
      "key": "user:42:prefs",
      "encoding": "json",
      "version": 7,
      "expires_at": null,
      "created_at": 1760486400000,
      "updated_at": 1760487000000
    },
    {
      "key": "user:42:session",
      "encoding": "json",
      "version": 12,
      "expires_at": 1760490600000,
      "created_at": 1760487000000,
      "updated_at": 1760487000000
    }
  ],
  "next_cursor": "dXNlcjo0MjpzZXNzaW9u"
}
```

## Get a key

`GET /v1/primitives/{id}/keys/{key}`

Returns the KvEntry, with its value. A key that doesn't exist or has expired answers `404 not_found`.

Key tool: `kv_get`.

```bash
curl https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/keys/user:42:prefs \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

```json
{
  "key": "user:42:prefs",
  "value": { "theme": "dark", "lang": "en" },
  "encoding": "json",
  "version": 7,
  "expires_at": null,
  "created_at": 1760486400000,
  "updated_at": 1760487000000
}
```

## Set a key

`PUT /v1/primitives/{id}/keys/{key}`

Creates or replaces a key and returns its KvEntry. Without `ttl` the key never expires, and any expiry it had is cleared: pass `ttl` on every write to keep a key expiring. A value is at most 1 MiB, as serialized JSON or as bytes (`413 payload_too_large`). A failed `if_version` is `412 precondition_failed`. On a Starter workspace at its storage cap, it answers `429 plan_limit`; see [Usage and limits](/docs/api/limits#usage-and-limits).

Key tool: `kv_set`.

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `value` | any | Required. Any JSON value, `null` included. With `encoding: "bytes"`, a base64 string. |
| `encoding` | string | `"json"` or `"bytes"`. Default `"json"`. |
| `ttl` | duration | How long the key lives, from 1 second to 365 days, such as `"1h"`. See [Durations](/docs/api#durations). |
| `if_version` | integer | Write only at this version; `0` means the key must not exist. See [Versions](#versions-and-ifversion). |

```bash
curl -X PUT https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/keys/session:abc \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value": {"step": 3}, "ttl": "1h", "if_version": 0}'
```

```json
{
  "key": "session:abc",
  "value": { "step": 3 },
  "encoding": "json",
  "version": 13,
  "expires_at": 1760490000000,
  "created_at": 1760486400000,
  "updated_at": 1760486400000
}
```

A version mismatch answers `412 precondition_failed`:

```json
{
  "error": {
    "code": "precondition_failed",
    "message": "key \"session:abc\": expected version 0, but it is at version 13"
  }
}
```

## Delete a key

`DELETE /v1/primitives/{id}/keys/{key}`

Deletes a key. Returns `{ deleted }`: `true` if a live key was removed, `false` if there was none. Deleting a missing key isn't an error unless you pass `if_version`, which fails with `412 precondition_failed` when the key isn't at that version.

Key tool: `kv_delete`.

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `if_version` | integer | Delete only at this version. |

```bash
curl -X DELETE "https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/keys/session:abc?if_version=13" \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

```json
{ "deleted": true }
```

## Get many keys

`POST /v1/primitives/{id}/mget`

Gets up to 100 keys in one call. `data` is in the same order as `keys`, with `null` for a key that doesn't exist or has expired.

Key tool: `kv_get`.

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `keys` | string[] | Required. 1–100 keys. |

```bash
curl -X POST https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/mget \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keys": ["task:1", "task:2"]}'
```

```json
{
  "data": [
    {
      "key": "task:1",
      "value": { "status": "ready" },
      "encoding": "json",
      "version": 21,
      "expires_at": null,
      "created_at": 1760486400000,
      "updated_at": 1760486400000
    },
    null
  ]
}
```

## Set many keys

`POST /v1/primitives/{id}/mset`

Sets up to 100 keys and returns their entries, in order. Each item takes the same fields as [Set a key](#set-a-key), plus its `key`.

Atomic by default: every item is written in one transaction, and if any item fails, such as an `if_version` mismatch, nothing is written and the call fails with an error whose message starts with the item's index, such as `items[1]: …`. With `atomic: false`, each item is written in its own transaction, in order, and the call stops at the first failure: earlier items stay written.

Every item is validated before anything is written, so an invalid key, `ttl` or value fails the call either way. On a Starter workspace at its storage cap, it answers `429 plan_limit`; see [Usage and limits](/docs/api/limits#usage-and-limits).

Key tool: `kv_set`.

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `items` | object[] | Required. 1–100 items of `{ key, value, encoding?, ttl?, if_version? }`. `key` and `value` are required. |
| `atomic` | boolean | Write all items in one transaction. Default `true`. |

```bash
curl -X POST https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/mset \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "key": "task:1", "value": { "status": "done" }, "if_version": 21 },
      { "key": "task:2", "value": { "status": "ready" }, "ttl": "1d" }
    ]
  }'
```

```json
{
  "data": [
    {
      "key": "task:1",
      "value": { "status": "done" },
      "encoding": "json",
      "version": 22,
      "expires_at": null,
      "created_at": 1760486400000,
      "updated_at": 1760490000000
    },
    {
      "key": "task:2",
      "value": { "status": "ready" },
      "encoding": "json",
      "version": 23,
      "expires_at": 1760576400000,
      "created_at": 1760490000000,
      "updated_at": 1760490000000
    }
  ]
}
```

## Delete many keys

`POST /v1/primitives/{id}/mdelete`

Deletes up to 100 keys in one transaction. Returns `{ deleted }`, the number of live keys removed. Missing keys are skipped.

Key tool: `kv_delete`.

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `keys` | string[] | Required. 1–100 keys. |

```bash
curl -X POST https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/mdelete \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keys": ["task:1", "task:2", "task:3"]}'
```

```json
{ "deleted": 2 }
```

## Increment a counter

`POST /v1/primitives/{id}/incr`

Adds `by` to an integer atomically and returns the new KvEntry. A missing or expired key starts at `0`. The value must be a JSON integer that fits in 64 bits; any other value, or a result that would overflow, is `400 invalid_request`. Unlike a set, `incr` keeps the key's expiry unless you pass `ttl`. On a Starter workspace at its storage cap, it answers `429 plan_limit`; see [Usage and limits](/docs/api/limits#usage-and-limits).

Key tool: `kv_set`.

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `key` | string | Required. The counter's key. |
| `by` | integer | Amount to add, may be negative. Default `1`. |
| `ttl` | duration | Reset the expiry to this, from 1 second to 365 days. See [Durations](/docs/api#durations). |

```bash
curl -X POST https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/incr \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key": "rate:agent-42", "by": 1, "ttl": "1m"}'
```

```json
{
  "key": "rate:agent-42",
  "value": 5,
  "encoding": "json",
  "version": 31,
  "expires_at": 1760486460000,
  "created_at": 1760486380000,
  "updated_at": 1760486400000
}
```

## Shapes

| Shape | Fields |
| --- | --- |
| KvEntry | `{ key, value, encoding, version, expires_at, created_at, updated_at }`. `encoding` is `"json"` or `"bytes"`; bytes are base64 in `value`. Listings leave `value` out unless `values=true`. `expires_at` is `null` for a key that doesn't expire |
| KvPage | `{ data: KvEntry[], next_cursor }`. `next_cursor` is `null` on the last page |

Scoped API keys need the tool named in each section on their allowlist; see [Tool allowlists](/docs/api/authentication#tool-allowlists). For creating, snapshotting, forking and pausing the store itself, see [Primitives](/docs/api/primitives). See [Limits](/docs/api/limits) for how operations are counted.
