Key-Value store
A managed key-value cache in every project, reachable over HTTP: read, write, delete, and atomically increment opaque values, with optional per-key TTLs. Server-side only — it answers to your project's secret key and to nothing else.
404, even with a
valid key.What it is
The store lives inside your own project database, in a kv
schema your project owns — no separate service to provision, no extension to
install, nothing to keep in sync. Values are opaque bytes: whatever you PUT
is what you GET back, byte for byte. The store never parses a value, with the
single exception of incr, which reads its
counter as a base-10 integer.
It is a cache, not a system of record. The backing table is unlogged, which keeps a cache workload's write traffic off the cluster's write-ahead log — at the documented cost that the table is emptied if the database server ever crashes and recovers. Keep anything you cannot regenerate in an ordinary table.
Watch your consumption on the project's Usage tab in the dashboard: the key count and stored bytes are shown against your plan's budgets, sampled hourly.
Authentication
Every KV request must carry your project's secret key
(kbs_…) as a bearer token:
Authorization: Bearer kbs_<your-secret-key>
Nothing else is accepted. A publishable key (kbp_…), an
end-user access token from Auth, and an unknown or
revoked credential all get 401. There is no per-key or per-user
policy over KV the way Row-Level Security covers your
tables, so the store is deliberately reachable only by callers you trust
completely.
Keys
A key is a single URL path segment: non-empty and at most 512
bytes, percent-encoded in the URL. An empty or over-long key is a
400. Because the key is one segment, use a separator that is not
/ when you namespace keys — : is the usual
choice, as in session:abc.
Read a value
GET /kv/v1/{key} returns the stored bytes with
Content-Type: application/octet-stream, or 404 when
the key does not exist or has expired.
curl https://<project-ref>.kethosbase.com/kv/v1/greeting \
-H "Authorization: Bearer kbs_<your-secret-key>"
hello
Write a value
PUT /kv/v1/{key} stores the request body as the value and
answers 204 No Content. Writing an existing key overwrites it.
curl -X PUT https://<project-ref>.kethosbase.com/kv/v1/greeting \
-H "Authorization: Bearer kbs_<your-secret-key>" \
--data-binary 'hello'
Add a TTL with the Kv-Ttl-Seconds header — a non-negative
integer number of seconds:
curl -X PUT https://<project-ref>.kethosbase.com/kv/v1/session:abc \
-H "Authorization: Bearer kbs_<your-secret-key>" \
-H "Kv-Ttl-Seconds: 300" \
--data-binary '{"user":"u_123"}'
Kv-Ttl-Seconds | Effect |
|---|---|
| absent | The key never expires. This is the default. |
0 | The key expires immediately — a subsequent read is a 404. |
N > 0 | The key expires N seconds after the write. |
| anything else | 400 — a negative or non-integer value is refused. |
Each write sets the TTL afresh: a PUT without the header on a key that had one clears its expiry. An expired key stops being readable the moment it lapses; the space it held is reclaimed by a background sweep, which also releases it from your budgets.
Delete a key
DELETE /kv/v1/{key} removes the key and reports whether it
was there. It is idempotent — deleting a missing key is a success, not an
error.
curl -X DELETE https://<project-ref>.kethosbase.com/kv/v1/greeting \
-H "Authorization: Bearer kbs_<your-secret-key>"
{"deleted":true}
Increment a counter
POST /kv/v1/{key}/incr atomically adds a delta to the integer
counter stored at the key and returns the running total. The key is created
at zero first, so you never have to seed a counter. The update happens in a
single statement, so concurrent callers never lose an increment.
curl -X POST https://<project-ref>.kethosbase.com/kv/v1/hits/incr \
-H "Authorization: Bearer kbs_<your-secret-key>"
{"value":1}
curl -X POST https://<project-ref>.kethosbase.com/kv/v1/hits/incr \
-H "Authorization: Bearer kbs_<your-secret-key>" \
-d '{"by":4}'
{"value":5}
The body may be a JSON object {"by": N}, a raw base-10
integer, or empty (which means +1). N may be
negative, so incr decrements too. The counter is stored as its
base-10 text form, which means a plain GET reads it back as
digits.
incr never sets a TTL. Incrementing a key whose TTL has
already lapsed restarts the counter at zero and clears the stale expiry.
Incrementing a key whose value is not an integer is a 400.
Errors
Errors use the same JSON body as the REST API —
message, code, details,
hint — with a KV… code:
{"message":"key not found","code":"KV404","details":null,"hint":null}
| Status | Code | When |
|---|---|---|
400 | KV400 | Empty or over-long key, a malformed Kv-Ttl-Seconds, or a malformed incr body. |
400 | a 22… code | incr on a key whose stored value is not an integer. |
401 | KV401 | Missing bearer token, or a credential that is not a live secret key for this project. |
404 | KV404 | The key does not exist or has expired — or KV is not enabled for the project. The two are deliberately indistinguishable to a caller. |
413 | KV413 | The value is larger than your plan's maximum value size. |
429 | KV429 | The project spent its monthly request or egress allowance. |
503 | KV503 | The project is being upgraded; retry shortly. |
507 | KV507 | The project reached its key-count or storage budget. The message says which. |
A 507 fails the write closed rather than evicting something
of yours to make room: nothing you stored is ever silently discarded to admit
a new key. Delete keys, add TTLs so they retire themselves, or move to a
larger plan.
Quotas
Three ceilings apply per project. Max value size bounds a
single value (413); keys bounds how many live
keys the project holds and storage bounds their total
on-disk footprint (both 507).
| Plan | Max value size | Keys | Storage |
|---|---|---|---|
| Starter | 256 KiB | 10,000 | 10 MiB |
| Pro | 256 KiB | 1,000,000 | 256 MiB |
| Business | 256 KiB | 10,000,000 | 2 GiB |
| Enterprise | 256 KiB | 100,000,000 | 16 GiB |
Enterprise ceilings are a starting point, not a wall — talk to us if your workload needs more.
The storage figure is the physical footprint of the store — values, keys, the index, and the database's own per-row overhead — not the sum of your value lengths, so expect it to run somewhat ahead of the bytes you wrote. Expired keys still occupy space, and still count, until the hourly sweep reclaims them.
KV requests are ordinary data-API requests: they count toward your
project's request allowance and daily ceiling, and the bytes a
GET returns count as egress. See
Limits & errors.
Limits
- There is no list, scan, or key-prefix search: you read a key you already know. Keep your own index in a table if you need to enumerate.
- There is no bulk get or bulk set — one key per request.
- There is no Redis wire protocol; this is an HTTP API, so a Redis client library cannot talk to it.
- No pub/sub, no lists, sets, or hashes — the value is opaque bytes and
the only structured operation is integer
incr. - The store is not covered by Row-Level Security and is not reachable with an end-user token, so it cannot be read directly by your app's users.