# Telnyx Storage: KV — Full Documentation > Complete page content for KV (Storage section) of the Telnyx developer docs (https://developers.telnyx.com). > This file: https://developers.telnyx.com/docs/development/llms/storage-kv-llms-full-txt.md · Root index: https://developers.telnyx.com/llms.txt ## Get Started ### KV > Source: https://developers.telnyx.com/docs/edge-compute/kv.md KV is a globally distributed key-value store: you write bytes under a string key and read them back, fast, from anywhere. It is built for read-heavy edge workloads — session data, cached responses, feature flags, and other small values a function needs on every request. A value is **opaque bytes**. You choose the serialization (text, JSON, binary); KV stores exactly what you send and returns it byte-for-byte. There is no envelope, no base64 encoding, and no server-side interpretation of the value. ## Two Ways to Use KV The same namespaces and keys are reachable two ways. Pick based on where your code runs. | | `env` binding | REST API | |---|---|---| | **Where** | Inside a TypeScript edge function | Anywhere — any language, any host | | **Auth** | Injected by the runtime; no API key in your code | Your `TELNYX_API_KEY` as a bearer token | | **Shape** | `env..get(...)`, `.put(...)`, `.delete(...)` | `GET`/`PUT`/`DELETE https://api.telnyx.com/v2/storage/kvs/{id}/keys/{key}` | | **Use it for** | Reads and writes on the request path from your function | Provisioning namespaces, non-TS runtimes, tooling, back-office scripts | Both hit the same store, so a value written through the binding is immediately readable over REST and vice versa. The binding is a thin, pre-authenticated wrapper over the same REST endpoints — it just means your function never handles an API key. The complete endpoint reference — namespaces and keys, with request/response schemas and code samples — is generated from the OpenAPI spec and lives in the **REST API** group of this KV section in the sidebar. The `env` KV binding is **TypeScript-only** and requires `@telnyx/edge-runtime` ≥ 0.2.2. Go, JS, Python, and Quarkus functions use the [REST API](/docs/edge-compute/kv/quick-start#path-b-the-rest-api) directly. ## Next Steps - [Quick Start](/docs/edge-compute/kv/quick-start) — Create a namespace, bind it, read and write - [How KV Works](/docs/edge-compute/kv/concepts/how-kv-works) — Keys, TTL, and the consistency model - [Examples](/docs/edge-compute/kv/examples/session-storage) — Session storage, caching, and feature flags - [Runtime API](/docs/edge-compute/kv/reference) — the `env` binding surface (`KvNamespace`) - [CLI Commands](/docs/edge-compute/kv/cli) — Manage KV from the command line - [Pricing](/docs/edge-compute/kv/pricing) — Free tier and paid plans ## Related Resources - [Bindings](/docs/edge-compute/runtime/bindings) — How the `env` binding surface works - [Secrets](/docs/edge-compute/configuration/secrets) — Secure credential storage --- ### Quick Start > Source: https://developers.telnyx.com/docs/edge-compute/kv/quick-start.md Get up and running with KV: create a namespace, then use it from a TypeScript function through an `env` binding, or from anywhere through the REST API. ## 1. Create a Namespace A namespace is an isolated key space. Create one with the CLI or the API. ```bash telnyx-edge storage kv create --name my-cache ``` ```bash curl -X POST https://api.telnyx.com/v2/storage/kvs \ -H "Authorization: Bearer $TELNYX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "my-cache"}' ``` The response includes the namespace `id` (a UUID) — you'll need it in the next step. A new namespace starts in `status: "pending"` and isn't writable yet: writes return `409` (`"Namespace is not ready (status: pending)"`) until provisioning finishes, which typically takes a few seconds and can stretch to ~20. If you're scripting, poll `GET https://api.telnyx.com/v2/storage/kvs/{id}` until `"status": "provision_ok"` before your first write. (With the binding path below you rarely notice — editing `func.toml` and deploying already takes longer than provisioning.) ## Path A: The Function Binding Recommended for TypeScript edge functions. The runtime injects the credential, so your code holds **no API key**. ### 2. Bind the Namespace Declare the namespace in `func.toml`. The block key is a name **you choose** — it's not a reserved word — and it becomes the property on `env`. This example uses `MY_KV`, so the binding is reached as `env.MY_KV`: ```toml [edge_compute] func_name = "my-function" [storage.kv.MY_KV] id = "550e8400-e29b-41d4-a716-446655440000" # Namespace ID from step 1 ``` Add `@telnyx/edge-runtime` (≥ 0.2.2) to your `package.json` dependencies, then regenerate the environment types: ```bash telnyx-edge types # writes telnyx-env.d.ts — env.MY_KV is now a typed KvNamespace ``` Each `[storage.kv.]` block becomes `env.: KvNamespace` in the generated `telnyx-env.d.ts` — declare as many namespaces as you need. KV type generation requires CLI **≥ v0.2.3** (earlier releases report the block as an unrecognized key and write an empty `Env`). The binding itself **resolves at runtime** from `func.toml` — types are for the compiler, and a stale `telnyx-env.d.ts` doesn't affect the deployed function. ### 3. Use `env.MY_KV` in Your Code ```ts import { env } from "@telnyx/edge-runtime"; // Write — value is stored verbatim (UTF-8 preserved) await env.MY_KV.put("user/123", JSON.stringify({ name: "Alice 👋" })); // Read as text -> '{"name":"Alice 👋"}' (null if the key is missing) const raw = await env.MY_KV.get("user/123"); // Read and JSON.parse in one step -> { name: "Alice 👋" } (null if missing) const user = await env.MY_KV.get<{ name: string }>("user/123", { type: "json" }); // Write with a server-side TTL — the key deletes itself after ~60 seconds await env.MY_KV.put("otp/123", "482913", { expirationTtl: 60 }); // Delete (idempotent — deleting a missing key is not an error) await env.MY_KV.delete("user/123"); ``` The binding surface: | Method | Returns | Notes | |--------|---------|-------| | `get(key)` | `Promise` | Raw stored text; `null` if the key doesn't exist | | `get(key, { type: "json" })` | `Promise` | `JSON.parse`s the value; `null` if missing | | `put(key, value, options?)` | `Promise` | `value` is a string, stored verbatim; `options.expirationTtl` expires the key server-side after that many seconds | | `delete(key)` | `Promise` | Idempotent | | `list(options?)` | `Promise<{ keys, list_complete, cursor? }>` | Returns key names and per-key `sizeBytes`/`updatedAt`, not values | `list()` returns key metadata, not values — `{ keys: [{ name, sizeBytes, updatedAt }], list_complete, cursor? }`. Paginate by passing the returned `cursor` back in `list({ cursor })`. On 0.2.1 entries carry only `name`; 0.2.0 throws a response-shape error. `put`'s `expirationTtl` option requires ≥ 0.2.2 — earlier versions accept it but silently ignore it. The `metadata` option is deprecated and ignored on every version. See [Key Expiration](/docs/edge-compute/kv/ttl-and-metadata). ## Path B: The REST API Use this anywhere outside a TypeScript edge function — a non-TypeScript function (Go, JS, Python, Quarkus), your own backend, or tooling. Authenticate with your `TELNYX_API_KEY` (the SDKs read it from the environment). Whether you use an SDK or plain HTTP, the value **is** the raw request/response body — no base64, no envelope. KV support landed in the official server SDKs in **telnyx-node ≥ 7.5.0**, **telnyx-python ≥ 4.166.0**, **telnyx-php ≥ 7.88.0** (see the PHP tab for the required version pin), **telnyx-ruby ≥ 5.152.0**, and **telnyx-go ≥ v4.85.0** — on earlier versions the `storage` resource is object storage (buckets) only. The Java SDK doesn't cover KV yet; call the endpoints over plain HTTP as in the curl tab. ```javascript import Telnyx from "telnyx"; const client = new Telnyx(); // reads TELNYX_API_KEY from the environment const kv = client.storage.kvs.keys; const id = process.env.KV_NAMESPACE_ID; // Write — the body is stored verbatim (UTF-8 preserved, no base64, no envelope) await kv.update("user/123", { id, body: JSON.stringify({ name: "Alice 👋" }) }); // Read — the response body is the raw stored value; a missing key throws NotFoundError async function kvGet(key) { try { const res = await kv.retrieve(key, { id }); return await res.text(); } catch (err) { if (err instanceof Telnyx.NotFoundError) return null; // Key not found throw err; } } const raw = await kvGet("user/123"); // '{"name":"Alice 👋"}' (null if missing) // Write with a server-side TTL — the key deletes itself after ~60 seconds await kv.update("otp/123", { id, body: "482913", ttl_secs: 60 }); // Delete (idempotent — deleting a missing key is not an error) await kv.delete("user/123", { id }); // List by prefix — returns key names and metadata (size_bytes, updated_at), never values const page = await kv.list(id, { prefix: "user/", limit: 100 }); // page.data -> [{ key, size_bytes, updated_at }] // When page.meta.has_more is true, pass page.meta.cursor back as { cursor } for the next page ``` ```python import os from telnyx import Telnyx, NotFoundError client = Telnyx() # reads TELNYX_API_KEY from the environment KV_NAMESPACE_ID = os.environ["KV_NAMESPACE_ID"] # Write — pass bytes; they are stored verbatim (UTF-8 preserved, no base64, no envelope) # (a plain str would be JSON-serialized by the SDK — quoted and escaped — before storage) client.storage.kvs.keys.update("user/123", id=KV_NAMESPACE_ID, body='{"name": "Alice 👋"}'.encode()) # Read — the response body is the raw stored value def kv_get(key: str) -> str | None: try: return client.storage.kvs.keys.retrieve(key, id=KV_NAMESPACE_ID).text() except NotFoundError: return None # Key not found kv_get("user/123") # '{"name": "Alice 👋"}' kv_get("missing/nope") # None # Write with a server-side TTL — the key deletes itself after ~60 seconds client.storage.kvs.keys.update("otp/123", id=KV_NAMESPACE_ID, body=b"482913", ttl_secs=60) # Delete (idempotent — deleting a missing key is not an error) client.storage.kvs.keys.delete("user/123", id=KV_NAMESPACE_ID) # List by prefix — returns key names and metadata (size_bytes, updated_at), never values page = client.storage.kvs.keys.list(KV_NAMESPACE_ID, prefix="user/", limit=100) for entry in page.data: print(entry.key, entry.size_bytes, entry.updated_at) # When page.meta.has_more is true, pass page.meta.cursor back as cursor= for the next page ``` ```php storage->kvs->keys; // Write — the value is stored verbatim (UTF-8 preserved, no base64, no envelope). // v7.88.0's $kv->update() throws a TypeError on every body type, so write through // $client->request(); pass the key as-is — the SDK URL-encodes the "/" for you. $kvPut = function (string $key, string $value, ?int $ttlSecs = null) use ($client, $id): void { $client->request( method: 'put', path: ['storage/kvs/%1$s/keys/%2$s', $id, $key], query: $ttlSecs === null ? [] : ['ttl_secs' => $ttlSecs], headers: ['Content-Type' => 'application/octet-stream'], body: $value, ); }; $kvPut('user/123', '{"name":"Alice 👋"}'); // Read — retrieve() returns the raw stored value as a string; a missing key throws NotFoundException $kvGet = function (string $key) use ($kv, $id): ?string { try { return $kv->retrieve($key, $id); } catch (NotFoundException) { return null; // Key not found } }; $raw = $kvGet('user/123'); // '{"name":"Alice 👋"}' (null if missing) // Write with a server-side TTL — the key deletes itself after ~60 seconds $kvPut('otp/123', '482913', ttlSecs: 60); // Delete (idempotent — deleting a missing key is not an error) $kv->delete('user/123', $id); // List by prefix — returns key names and metadata (size_bytes, updated_at), never values $page = $kv->list($id, prefix: 'user/', limit: 100); foreach ($page->data as $entry) { echo "{$entry->key} {$entry->sizeBytes} bytes {$entry->updatedAt->format(DATE_RFC3339)}\n"; } // When $page->meta->hasMore is true, pass $page->meta->cursor back as cursor: for the next page ``` Install with `composer require "telnyx/telnyx-php:^7.88" guzzlehttp/guzzle`. The pin matters: the semver-highest tag v8.0.0 predates KV, so an unpinned `composer require telnyx/telnyx-php` installs a version with no `storage->kvs` at all. Guzzle (or any PSR-18 client) is required because the SDK doesn't bundle one — without it `new Client()` throws a discovery exception. ```go package kv import ( "context" "errors" "io" "os" "strings" "github.com/team-telnyx/telnyx-go/v4" "github.com/team-telnyx/telnyx-go/v4/option" ) var ( namespaceID = os.Getenv("KV_NAMESPACE_ID") client = telnyx.NewClient() // reads TELNYX_API_KEY from the environment ) // Write — pass the value with option.WithRequestBody so it is stored verbatim // (don't use the params Body field: it multipart-encodes the value) func kvPut(ctx context.Context, key, value string) error { return client.Storage.Kvs.Keys.Update(ctx, key, telnyx.StorageKvKeyUpdateParams{ID: namespaceID}, option.WithRequestBody("application/octet-stream", strings.NewReader(value))) } func kvGet(ctx context.Context, key string) (string, error) { resp, err := client.Storage.Kvs.Keys.Get(ctx, key, telnyx.StorageKvKeyGetParams{ID: namespaceID}) var apierr *telnyx.Error if errors.As(err, &apierr) && apierr.StatusCode == 404 { return "", nil // Key not found } if err != nil { return "", err } defer resp.Body.Close() value, err := io.ReadAll(resp.Body) return string(value), err // Raw stored bytes } // TtlSecs expires the key server-side after that many seconds func kvPutTTL(ctx context.Context, key, value string, ttlSecs int64) error { return client.Storage.Kvs.Keys.Update(ctx, key, telnyx.StorageKvKeyUpdateParams{ID: namespaceID, TtlSecs: telnyx.Int(ttlSecs)}, option.WithRequestBody("application/octet-stream", strings.NewReader(value))) } // Idempotent — deleting a missing key is not an error func kvDelete(ctx context.Context, key string) error { return client.Storage.Kvs.Keys.Delete(ctx, key, telnyx.StorageKvKeyDeleteParams{ID: namespaceID}) } // Returns key names + metadata (size_bytes, updated_at), never values. // When Meta.HasMore is true, pass Meta.Cursor back to fetch the next page. func kvList(ctx context.Context, prefix, cursor string) (*telnyx.StorageKvKeyListResponse, error) { params := telnyx.StorageKvKeyListParams{Prefix: telnyx.String(prefix), Limit: telnyx.Int(100)} if cursor != "" { params.Cursor = telnyx.String(cursor) } return client.Storage.Kvs.Keys.List(ctx, namespaceID, params) } ``` ```ruby require "telnyx" client = Telnyx::Client.new # reads ENV["TELNYX_API_KEY"] kv_id = ENV.fetch("KV_NAMESPACE_ID") kv = client.storage.kvs.keys # Write — the value is stored verbatim (UTF-8 preserved, no base64, no envelope) kv.update("user/123", id: kv_id, body: '{"name":"Alice 👋"}') # Read — returns the raw stored value; a missing key raises NotFoundError value = begin kv.retrieve("user/123", id: kv_id).read rescue Telnyx::Errors::NotFoundError nil # Key not found end # Write with a server-side TTL — the key deletes itself after ~60 seconds kv.update("otp/123", id: kv_id, body: "482913", ttl_secs: 60) # Delete (idempotent — deleting a missing key is not an error) kv.delete("user/123", id: kv_id) # List by prefix — returns key names and metadata (size_bytes, updated_at), never values page = kv.list(kv_id, prefix: "user/", limit: 100) page.data.each { |k| puts "#{k.key} #{k.size_bytes} bytes #{k.updated_at}" } # When page.meta.has_more is true, pass page.meta.cursor back as cursor: for the next page ``` The gem requires Ruby ≥ 3.2. On Ruby ≥ 3.4, also `gem install base64` — telnyx 5.152.0 loads it but doesn't yet declare it as a dependency. ```bash # Write — the request body is stored verbatim (no base64, no envelope) curl -X PUT "https://api.telnyx.com/v2/storage/kvs/$KV_NAMESPACE_ID/keys/user/123" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ --data-binary '{"name":"Alice 👋"}' # Read — the response body is the raw stored value (404 if the key doesn't exist) curl "https://api.telnyx.com/v2/storage/kvs/$KV_NAMESPACE_ID/keys/user/123" \ -H "Authorization: Bearer $TELNYX_API_KEY" # Write with a server-side TTL — the key deletes itself after ~60 seconds curl -X PUT "https://api.telnyx.com/v2/storage/kvs/$KV_NAMESPACE_ID/keys/otp/123?ttl_secs=60" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ --data-binary '482913' # Delete (idempotent — deleting a missing key is not an error) curl -X DELETE "https://api.telnyx.com/v2/storage/kvs/$KV_NAMESPACE_ID/keys/user/123" \ -H "Authorization: Bearer $TELNYX_API_KEY" # List keys by prefix — returns key names and metadata, never values curl "https://api.telnyx.com/v2/storage/kvs/$KV_NAMESPACE_ID/keys?prefix=user/&limit=100" \ -H "Authorization: Bearer $TELNYX_API_KEY" ``` Server-side TTL (`ttl_secs`), its error cases, and an inspectable application-level alternative are covered in [Key Expiration](/docs/edge-compute/kv/ttl-and-metadata). `list` returns key names and per-key metadata, never values: ```json { "record_type": "storage_kv_key", "data": [{ "key": "user/123", "size_bytes": 21, "updated_at": "2026-06-18T14:48:17.475129983Z" }], "meta": { "has_more": false } } ``` When `meta.has_more` is `true`, pass the returned `meta.cursor` back as `?cursor=` (in the SDKs, the `cursor` parameter) to fetch the next page — key listing does not auto-paginate in any SDK. Inside an edge function, the org binding injects `TELNYX_API_KEY` (and a base-URL proxy) at runtime, so REST calls from a function authenticate without you shipping a key. Next: [Best Practices](/docs/edge-compute/kv/best-practices) for key naming, serialization, and error handling. --- ## Concepts ### How KV Works > Source: https://developers.telnyx.com/docs/edge-compute/kv/concepts/how-kv-works.md KV is a single global key-value store optimized for low-latency reads from edge functions. A value is **opaque bytes** — you choose the serialization (text, JSON, binary), and KV stores exactly what you send and returns it byte-for-byte, with no envelope, base64 encoding, or server-side interpretation. ## Keys A key is a path-like string. Allowed characters are `a-z`, `A-Z`, `0-9`, and `-` `_` `/` `=` `.`. Use `/` to group related keys (for example `user/123`, `session/abc`). Colons (`:`) are **not** allowed. ## Expiration (TTL) By default a value lives until you delete it. You can also set a **server-side TTL** so a key expires automatically: pass `expirationTtl` on a binding `put` (`env.MY_KV.put(key, value, { expirationTtl: 30 })`, requires `@telnyx/edge-runtime` ≥ 0.2.2), `ttl_secs` on a REST write (`PUT …/keys/{key}?ttl_secs=N`), or `--ttl` on the CLI (`telnyx-edge storage kv key put … --ttl 30s`). The TTL is a whole number of seconds; once it elapses the key is gone and reads return `null`/`404`. See [Key Expiration](/docs/edge-compute/kv/ttl-and-metadata). ## No Per-Key Metadata KV has **no per-key metadata**. The binding's `put` accepts a `metadata` option so older code keeps compiling, but it is ignored (deprecated as of 0.2.2), and `list` never returns metadata. ## Consistency and Regionality KV is a **single global store**. There is no region to choose at creation time and no per-region copies to reconcile — every namespace is one logical dataset reachable from every edge location. Writes are replicated for durability and committed by quorum before they're acknowledged. - **Read-your-writes** from a given location is reliable: once a write returns, a subsequent read sees it. - **Across locations**, a read issued immediately after a write elsewhere may briefly observe the previous value; treat cross-location visibility as near-real-time rather than instantaneous. - **Distance costs latency, not staleness** — a location far from where the data is coordinated pays network round-trip on the request, but reads the same authoritative data as everywhere else. - **No transactions or compare-and-swap.** Don't use KV for atomic read-modify-write, counters, or coordination — concurrent writers to one key are last-write-wins. --- ### Key Expiration > Source: https://developers.telnyx.com/docs/edge-compute/kv/ttl-and-metadata.md KV supports **server-side expiration (TTL)**: set a TTL on a write and the key is deleted automatically once it elapses. Without a TTL, a value lives until you delete it. KV has **no per-key metadata**. ## Server-Side TTL Pass `expirationTtl` on a binding `put`, a `ttl_secs` query parameter on a REST write, or `--ttl` on the CLI. The value is a whole number of seconds (`1`–`9223372036`); the key expires roughly that many seconds after the write, after which reads return `null`/`404`. ```ts import { env } from "@telnyx/edge-runtime"; // Expire this key ~30 seconds after writing await env.MY_KV.put("session/abc", JSON.stringify({ userId: 42 }), { expirationTtl: 30 }); ``` ```bash # Expire this key ~30 seconds after writing curl -X PUT "https://api.telnyx.com/v2/storage/kvs/$KV_NAMESPACE_ID/keys/session%2Fabc?ttl_secs=30" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ --data-binary '{"userId": 42}' ``` ```bash telnyx-edge storage kv key put session/abc '{"userId": 42}' --ttl 30s ``` An invalid `ttl_secs` (non-integer, `0`, or negative) is rejected with `422` and the key is not written. The binding never produces that `422`: it floors `expirationTtl` to a whole number of seconds and, if the result is less than `1`, sends no TTL at all — the write succeeds and the key does not expire. There is no way to read the remaining TTL back — a `get`/`list` on a live key does not report its expiry. `expirationTtl` requires `@telnyx/edge-runtime` **≥ 0.2.2**. Earlier versions accept the option but silently ignore it — the key is written without a TTL. ## No Per-Key Metadata KV stores values as opaque bytes and has **no per-key metadata**. The binding's `put` still accepts a `metadata` option so older code keeps compiling, but it is ignored (and marked `@deprecated` as of `@telnyx/edge-runtime` 0.2.2), and `list` never returns metadata. Anything you put in the value itself (including JSON that looks like those fields) is stored verbatim, not interpreted. ## Application-Level Expiry Server-side TTL deletes the key and tells you nothing else — there is no way to read a key's remaining lifetime. Use this pattern instead when you want an absolute `expires_at` timestamp you can inspect on read (or when you're pinned to `@telnyx/edge-runtime` < 0.2.2, where `expirationTtl` is ignored). Wrap your value with the timestamp and check it when you read; if it's in the past, treat the key as missing (and optionally delete it). ```ts import { env } from "@telnyx/edge-runtime"; async function putWithExpiry(key: string, value: string, ttlSeconds: number) { await env.MY_KV.put(key, JSON.stringify({ value, expires_at: Date.now() + ttlSeconds * 1000, })); } async function getWithExpiry(key: string): Promise { const wrapped = await env.MY_KV.get<{ value: string; expires_at: number }>(key, { type: "json" }); if (wrapped === null) return null; // key not found if (Date.now() > wrapped.expires_at) { await env.MY_KV.delete(key); // lazily clean up return null; // expired } return wrapped.value; } ``` ```ts // Usage: a session that "expires" after one hour await putWithExpiry("session/abc", JSON.stringify({ userId: 42 }), 3600); const session = await getWithExpiry("session/abc"); // null once an hour has passed ``` Using the REST API instead? You'd normally reach for `ttl_secs` above. Build this envelope on top of the [REST API examples](/docs/edge-compute/kv/quick-start#path-b-the-rest-api) from the Quick Start only when you need the inspectable `expires_at`. Notes on this pattern: - **Reads do the enforcing.** An expired key still occupies storage until it's read (and lazily deleted) or you delete it explicitly. Prefer native TTL (`expirationTtl`/`ttl_secs`) for eager server-side cleanup; if you do need a sweep, drive it from an external scheduler hitting your function over HTTP — HTTP is the only function trigger today. - **Use a consistent clock.** `Date.now()` on the edge node is fine for coarse expiry; don't rely on it for sub-second precision. - **Keep the envelope small.** You pay for stored bytes, so the wrapper adds a little overhead per key. --- ## Examples ### Session Storage > Source: https://developers.telnyx.com/docs/edge-compute/kv/examples/session-storage.md Store user sessions at the edge and expire them after a day. This uses the [KV binding](/docs/edge-compute/kv/quick-start#path-a-the-function-binding) (bound as `MY_KV`) with a **server-side TTL**: pass `expirationTtl` on the write and KV deletes the session automatically once it elapses. Each write renews the TTL, so an active session slides forward and an abandoned one expires. Requires `@telnyx/edge-runtime` ≥ 0.2.2. ```ts import { env } from "@telnyx/edge-runtime"; const SESSION_TTL = 86_400; // 24 hours interface Session { created: number; views: number; } export async function handler(request: Request): Promise { const sessionId = request.headers.get("X-Session-ID"); if (!sessionId) { return new Response("Missing X-Session-ID", { status: 400 }); } const key = `session/${sessionId}`; // Read session (null if missing or expired) const existing = await env.MY_KV.get(key, { type: "json" }); const data = existing ? { ...existing, views: existing.views + 1 } : { created: Date.now(), views: 0 }; // Store with a 24h server-side TTL — renewed on every write await env.MY_KV.put(key, JSON.stringify(data), { expirationTtl: SESSION_TTL }); return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" }, }); } ``` Non-TypeScript functions get the same behavior with the `ttl_secs` parameter on a [REST API write](/docs/edge-compute/kv/quick-start#path-b-the-rest-api). If you need to inspect *when* a session expires, use the application-level envelope instead — see [Key Expiration](/docs/edge-compute/kv/ttl-and-metadata). --- ### API Response Caching > Source: https://developers.telnyx.com/docs/edge-compute/kv/examples/api-response-caching.md Cache expensive upstream responses for a few minutes. This uses the [KV binding](/docs/edge-compute/kv/quick-start#path-a-the-function-binding) (bound as `MY_KV`) with a **server-side TTL**: pass `expirationTtl` on the write and KV deletes the key automatically once it elapses — no cleanup code. Requires `@telnyx/edge-runtime` ≥ 0.2.2. ```ts import { env } from "@telnyx/edge-runtime"; const CACHE_TTL = 300; // 5 minutes export async function handler(request: Request): Promise { const cacheKey = `cache/api${new URL(request.url).pathname}`; // null once the TTL has elapsed (or if never cached) const cached = await env.MY_KV.get(cacheKey); if (cached !== null) { return new Response(cached, { headers: { "X-Cache": "HIT" } }); } const upstream = await fetch("https://api.example.com/data"); const data = await upstream.text(); await env.MY_KV.put(cacheKey, data, { expirationTtl: CACHE_TTL }); return new Response(data, { headers: { "X-Cache": "MISS" } }); } ``` Non-TypeScript functions get the same behavior with the `ttl_secs` parameter on a [REST API write](/docs/edge-compute/kv/quick-start#path-b-the-rest-api). If you need to inspect *when* an entry expires, use the application-level envelope instead — see [Key Expiration](/docs/edge-compute/kv/ttl-and-metadata). --- ### Feature Flags > Source: https://developers.telnyx.com/docs/edge-compute/kv/examples/feature-flags.md Read flags on the request path — no expiry needed, so use the [KV binding](/docs/edge-compute/kv/quick-start#path-a-the-function-binding) (`env.MY_KV`) directly. ```ts import { env } from "@telnyx/edge-runtime"; export async function handler(request: Request): Promise { const newUiEnabled = await env.MY_KV.get("flag/new-ui"); // Swap in whatever each branch should serve. return newUiEnabled === "true" ? new Response("new UI") : new Response("old UI"); } ``` Flip a flag without redeploying — from the CLI: ```bash telnyx-edge storage kv key put flag/new-ui true ``` --- ## Reference ### Overview > Source: https://developers.telnyx.com/docs/edge-compute/kv/reference.md The types in this reference are exported from `@telnyx/edge-runtime` (TypeScript) and describe version **≥ 0.2.2** — the first release where `expirationTtl` is applied and `list()` entries carry `sizeBytes`/`updatedAt`. They describe the [`env` binding](/docs/edge-compute/kv/quick-start#path-a-the-function-binding) — the in-function surface. To read or write KV from another language or outside a function, use the [REST API](/docs/edge-compute/kv/quick-start#path-b-the-rest-api). The KV Runtime API is a single binding type and the small set of option/result types its methods take. A namespace declared as `[storage.kv.]` in `func.toml` resolves on `env.` as a `KvNamespace`. | Surface | Where it lives | What it's for | |---|---|---| | [`KvNamespace`](/docs/edge-compute/kv/reference/kv-namespace) | `env.` | The binding handle — `get`, `put`, `delete`, `list`. | | [`KvGetTextOptions` / `KvGetJsonOptions`](/docs/edge-compute/kv/reference/kv-namespace#get-key-options) | `get()` options | Select the raw-text read or a `JSON.parse`d read. | | [`KvPutOptions`](/docs/edge-compute/kv/reference/kv-namespace#put-key-value-options) | `put()` options | `expirationTtl` — server-side TTL in seconds (`metadata` is deprecated and ignored). | | [`KvListOptions` / `KvListResult` / `KvKeyInfo`](/docs/edge-compute/kv/reference/kv-namespace#list-options) | `list()` options + result | Prefix, pagination cursor, and the returned key entries. | ## Getting the Binding ```ts import { env } from "@telnyx/edge-runtime"; // env.MY_KV is a KvNamespace, from [storage.kv.MY_KV] in func.toml await env.MY_KV.put("greeting", "hello"); const greeting = await env.MY_KV.get("greeting"); // "hello" ``` Declaring the binding is covered in the [Quick Start](/docs/edge-compute/kv/quick-start#path-a-the-function-binding). The binding resolves at runtime from `func.toml`; run `telnyx-edge types` (CLI ≥ v0.2.3) after editing the manifest to regenerate `telnyx-env.d.ts`, which types `env.` as a `KvNamespace`. ## Related Resources - [`KvNamespace`](/docs/edge-compute/kv/reference/kv-namespace) — the method-by-method reference - [Bindings](/docs/edge-compute/runtime/bindings) — how bindings resolve on `env` - [REST API](/docs/edge-compute/kv/quick-start#path-b-the-rest-api) — the same operations over HTTP - [Key Expiration](/docs/edge-compute/kv/ttl-and-metadata) — server-side TTL via `expirationTtl`, `ttl_secs`, or `--ttl` --- ### KvNamespace > Source: https://developers.telnyx.com/docs/edge-compute/kv/reference/kv-namespace.md `env.` (a `KvNamespace`) is the in-function handle to a KV namespace. It's a thin, pre-authenticated wrapper over the [KV REST API](/docs/edge-compute/kv/quick-start#path-b-the-rest-api) — the runtime injects the credential, so your code holds no API key. ```ts interface KvNamespace { get(key: string, options?: KvGetTextOptions): Promise; get(key: string, options: KvGetJsonOptions): Promise; put(key: string, value: string, options?: KvPutOptions): Promise; delete(key: string): Promise; list(options?: KvListOptions): Promise; } ``` Key behaviors: - **Values are opaque bytes** — `put` stores the string you pass verbatim (no base64, no envelope); `get` returns it byte-for-byte. - **Missing keys read as `null`** — `get` resolves to `null` for a key that doesn't exist, not an error. - **`delete` is idempotent** — deleting a missing key succeeds. - **Read-your-writes** — a read after a successful `put` from the same location reflects it. See [Consistency](/docs/edge-compute/kv/concepts/how-kv-works#consistency-and-regionality). - **Errors throw** — a non-2xx from the store (other than the `404`→`null` on `get`) rejects the promise with an `Error` describing the operation and status. ## `get(key, options?)` Read a value. Two overloads, selected by `options.type`: ```ts interface KvGetTextOptions { type?: "text" } // default interface KvGetJsonOptions { type: "json" } // JSON.parse the stored value ``` ```ts // Text (default) -> string | null const raw = await env.MY_KV.get("user/123"); // JSON -> T | null (JSON.parse applied; an empty stored value yields null) const user = await env.MY_KV.get<{ name: string }>("user/123", { type: "json" }); ``` Returns `null` if the key does not exist. With `{ type: "json" }`, a malformed stored value throws from `JSON.parse`. ## `put(key, value, options?)` Write a value. `value` is a string, stored verbatim. Resolves once the write is acknowledged. ```ts interface KvPutOptions { expirationTtl?: number; // seconds until the key expires server-side /** @deprecated Not supported by the API. Accepted but ignored. */ metadata?: unknown; } ``` ```ts await env.MY_KV.put("user/123", JSON.stringify({ name: "Alice" })); // Expire automatically after one hour await env.MY_KV.put("session/abc", token, { expirationTtl: 3600 }); ``` `expirationTtl` maps to the REST API's `?ttl_secs=` parameter: the key is deleted server-side roughly that many seconds after the write. The value is floored to a whole number of seconds; anything below `1` is not sent — the write succeeds without a TTL. See [Key Expiration](/docs/edge-compute/kv/ttl-and-metadata). `expirationTtl` requires `@telnyx/edge-runtime` **≥ 0.2.2** — earlier versions accept it but silently ignore it. `metadata` is ignored on every version (KV has no per-key metadata); it remains on the type, deprecated, so code that sets it keeps compiling. ## `delete(key)` Remove a key. Idempotent — deleting a missing key resolves normally. ```ts await env.MY_KV.delete("user/123"); ``` ## `list(options?)` Enumerate keys (names only — `list` does not return values). ```ts interface KvListOptions { prefix?: string; limit?: number; cursor?: string; // from a previous result's `cursor` } interface KvListResult { keys: KvKeyInfo[]; list_complete: boolean; cursor?: string; // present when list_complete is false } interface KvKeyInfo { name: string; sizeBytes?: number; // stored value size, from the API's size_bytes updatedAt?: Date; // last write time, from the API's updated_at /** @deprecated Not populated by the API. Always undefined. */ metadata?: unknown; } ``` ```ts const { keys } = await env.MY_KV.list({ prefix: "user/" }); // [{ name: "user/123", sizeBytes: 21, updatedAt: 2026-06-18T14:48:17.475Z }, …] ``` `list()` requires `@telnyx/edge-runtime` ≥ 0.2.1 — on 0.2.0 it throws `Unexpected KV list response shape`, because that release's parser predates the current API list format. `sizeBytes` and `updatedAt` are populated from **0.2.2**; on 0.2.1 entries carry only `name`. `KvKeyInfo.metadata` is never populated — KV has no per-key metadata. It remains on the type, deprecated, so code that reads it keeps compiling. ## Related - [Overview](/docs/edge-compute/kv/reference) — the KV Runtime API surface at a glance - [Quick Start](/docs/edge-compute/kv/quick-start#path-a-the-function-binding) — declare the binding and type it - [REST API](/docs/edge-compute/kv/quick-start#path-b-the-rest-api) — the same operations over HTTP --- ### CLI > Source: https://developers.telnyx.com/docs/edge-compute/kv/cli.md Manage KV namespaces and keys using the `telnyx-edge` CLI. ## Namespace Management ```bash # List all namespaces telnyx-edge storage kv list # Create a namespace (name: lowercase letters, numbers, hyphens) telnyx-edge storage kv create --name my-cache # Get a namespace telnyx-edge storage kv get # Delete a namespace telnyx-edge storage kv delete ``` ## Key Operations The value is stored verbatim — pass it as a positional argument, or use `--path` to store the contents of a file. ```bash # List keys in a namespace telnyx-edge storage kv key list # List keys filtered by prefix telnyx-edge storage kv key list --prefix user/ # Get a value (prints the raw stored bytes) telnyx-edge storage kv key get user/123 # Put a value telnyx-edge storage kv key put user/123 "hello" # Put a value from a file telnyx-edge storage kv key put user/123 --path ./value.json # Put a value with a server-side TTL (key expires after the duration) telnyx-edge storage kv key put session/abc "hello" --ttl 30s # Delete a key telnyx-edge storage kv key delete user/123 ``` ### Key Put Flags | Flag | Description | |------|-------------| | `--path` | Store the contents of a file as the value, instead of a positional argument | | `--ttl` | Server-side expiry as a duration (`30s`, `5m`, `1h`); the key is deleted after it elapses | ### Key List Flags | Flag | Description | |------|-------------| | `--prefix` | Filter keys by prefix | | `--cursor` | Pagination cursor from a previous response | | `--limit` | Maximum number of keys to return, `1`–`1000` (default `1000`) | Keys may contain `a-z`, `A-Z`, `0-9`, and `-` `_` `/` `=` `.` (no colons). Use `--ttl` for server-side expiry (see [Key Expiration](/docs/edge-compute/kv/ttl-and-metadata)). KV has no per-key metadata, so there is no `--metadata` flag. --- ### Best Practices > Source: https://developers.telnyx.com/docs/edge-compute/kv/best-practices.md Practical guidance for working with KV, whether through the [`env` binding](/docs/edge-compute/kv/quick-start#path-a-the-function-binding) or the [REST API](/docs/edge-compute/kv/quick-start#path-b-the-rest-api). ## Key Naming Keys may contain `a-z`, `A-Z`, `0-9`, and `-` `_` `/` `=` `.` (no colons). Use `/` to group related keys: ``` user/123 # User data session/abc # Session data cache/api/users # Cached API response flag/new-feature # Feature flag ``` Grouping by prefix also lets you enumerate a subset later — `list({ prefix: "user/" })` (or `?prefix=user/` over REST). ## Value Serialization KV stores values verbatim, so serialize complex values yourself (no base64 needed): ```ts // Write await env.MY_KV.put("user/123", JSON.stringify({ name: "Alice", age: 30 })); // Read + parse const user = await env.MY_KV.get<{ name: string; age: number }>("user/123", { type: "json" }); ``` ## Missing Keys `get` returns `null` for a key that doesn't exist — handle it explicitly: ```ts const value = await env.MY_KV.get("possibly-missing-key"); if (value === null) { return new Response("Not found", { status: 404 }); } ``` ## Keep Values Small KV is built for many small values read on the request path, not for large blobs. A value is capped at **1 MiB** (1,048,576 bytes) — a larger write is rejected with `413`. Store big or binary objects in [Cloud Storage](/docs/cloud-storage/quick-start) and keep only the key or a small reference in KV. ## Limits | Limit | Value | |-------|-------| | Max value size | 1 MiB (1,048,576 bytes) — over → `413` | | Max key length | 256 characters — over → `400` | | Key characters | `a-z` `A-Z` `0-9` `-` `_` `/` `=` `.` (no colons) | | `list` page size | `limit` 1–1000 (default 1000) | ## Don't Rely on Atomicity KV has no transactions or compare-and-swap, and concurrent writers to one key are last-write-wins. Don't use it for counters, locks, or coordination — see [Consistency](/docs/edge-compute/kv/concepts/how-kv-works#consistency-and-regionality). --- ## Platform ### Pricing > Source: https://developers.telnyx.com/docs/edge-compute/kv/pricing.md KV pricing is based on operations and storage. Egress is free. ## Pricing | Resource | Free Tier | Paid | |----------|-----------|------| | Reads | 10M/month | $0.35/million | | Writes | 1M/month | $3.50/million | | Deletes | 1M/month | $3.50/million | | Lists | 1M/month | $3.50/million | | Storage | 1 GB/month | $0.35/GB-month | Egress is free. No charges for data transferred out of KV. --- ## API Reference (KV) ### kv namespaces - [List KV namespaces](https://developers.telnyx.com/api-reference/kv-namespaces/list-kv-namespaces.md): Lists the KV namespaces for the authenticated user's organization. Results use page-based pagination (`page[number]`/`page[size]`). - [Create a KV namespace](https://developers.telnyx.com/api-reference/kv-namespaces/create-a-kv-namespace.md): Creates a new KV namespace. Provisioning is asynchronous: the namespace is returned with status `pending` and becomes usable once it reaches `provision_ok`. - [Get a KV namespace](https://developers.telnyx.com/api-reference/kv-namespaces/get-a-kv-namespace.md): Retrieves a KV namespace by its ID, including its provisioning status. - [Delete a KV namespace](https://developers.telnyx.com/api-reference/kv-namespaces/delete-a-kv-namespace.md): Deletes a KV namespace and all of the keys it contains. Deletion is asynchronous: the namespace is returned with status `deleting`. Deleting a namespace whose… ### kv keys - [List keys](https://developers.telnyx.com/api-reference/kv-keys/list-keys.md): Lists the keys in a namespace. Returns key names and metadata only, never values. Results are paginated with `limit` and an opaque `cursor`. - [Get a key's value](https://developers.telnyx.com/api-reference/kv-keys/get-a-keys-value.md): Returns the raw stored value for a key. The response body is the value exactly as it was written; the `Content-Type` header echoes the value's stored content t… - [Set a key's value](https://developers.telnyx.com/api-reference/kv-keys/set-a-keys-value.md): Creates or replaces the value for a key. The request body is stored verbatim as the value — no base64, no JSON envelope — up to 1 MiB. The request's `Content-T… - [Delete a key](https://developers.telnyx.com/api-reference/kv-keys/delete-a-key.md): Deletes a key. Idempotent: deleting a key that does not exist still succeeds. The namespace itself must exist and be provisioned. ### cloudfs filesystems - [List CloudFS filesystems](https://developers.telnyx.com/api-reference/cloudfs-filesystems/list-cloudfs-filesystems.md): Lists the CloudFS filesystems for the authenticated user's organization. Results use cursor-based pagination: fetch the next page by passing `meta.cursors.afte… - [Create a CloudFS filesystem](https://developers.telnyx.com/api-reference/cloudfs-filesystems/create-a-cloudfs-filesystem.md): Creates a CloudFS filesystem. Provisioning is synchronous — typically a few seconds, up to a few minutes — and the filesystem is returned with status `ready`,… - [Get a CloudFS filesystem](https://developers.telnyx.com/api-reference/cloudfs-filesystems/get-a-cloudfs-filesystem.md): Retrieves a CloudFS filesystem by its ID. The returned `meta_url` omits the credential — the metadata token is only ever returned by create and rotate-meta-tok… - [Update a CloudFS filesystem](https://developers.telnyx.com/api-reference/cloudfs-filesystems/update-a-cloudfs-filesystem.md): Updates a CloudFS filesystem. Only `name` can be changed; other fields are immutable and unknown fields are rejected with a `400`. Renaming to a name that alre… - [Delete a CloudFS filesystem](https://developers.telnyx.com/api-reference/cloudfs-filesystems/delete-a-cloudfs-filesystem.md): Permanently deletes a CloudFS filesystem, removing its S3 bucket and its metadata database. Deletion is synchronous: the response returns the filesystem's fina… - [Rotate the metadata token](https://developers.telnyx.com/api-reference/cloudfs-filesystems/rotate-the-metadata-token.md): Issues a new metadata access token for the filesystem and returns the full filesystem, including the new `meta_token` and credential-bearing `meta_url`. The pr…