# Telnyx Storage: KV — Full Documentation > Complete page content for KV (Storage section) of the Telnyx developer docs (https://developers.telnyx.com). > Root index: https://developers.telnyx.com/llms.txt · Lightweight index for this subsection: https://telnyx.com/llms/storage/kv.txt ## Key-Value (KV) ### KV Quick Start > Source: https://developers.telnyx.com/docs/edge-compute/kv/quick-start.md Get up and running with KV by creating a namespace, binding it to your function, and reading/writing data. ## 1. Create a Namespace ```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 namespace `name` may contain only lowercase letters, numbers, and hyphens. Namespace creation is asynchronous — poll the namespace endpoint until `status` changes from `pending` to `provision_ok`. ## 2. Add to Your Function Configure the binding in `func.toml`: ```toml [edge_compute] func_name = "my-function" [storage.kv.MY_CACHE] id = "550e8400-e29b-41d4-a716-446655440000" # Namespace ID returned by the create API (a UUID) ``` ## 3. Access KV in Your Code KV is accessed over HTTP (a native SDK is planned). A value is stored and returned **verbatim** — there is no base64 encoding and no JSON envelope, so you send the raw value as the request body and read the raw bytes back. ```javascript const KV_NAMESPACE_ID = process.env.KV_MY_CACHE_ID; const API_KEY = process.env.TELNYX_API_KEY; const BASE = `https://api.telnyx.com/v2/storage/kvs/${KV_NAMESPACE_ID}/keys`; async function kvGet(key) { const res = await fetch(`${BASE}/${encodeURIComponent(key)}`, { headers: { "Authorization": `Bearer ${API_KEY}` } }); if (res.status === 404) return null; // Key not found if (!res.ok) throw new Error(`KV read error: ${res.status}`); return await res.text(); // Value is the raw stored bytes } async function kvPut(key, value) { const res = await fetch(`${BASE}/${encodeURIComponent(key)}`, { method: "PUT", headers: { "Authorization": `Bearer ${API_KEY}` }, body: value // Stored verbatim — no base64, no envelope }); if (!res.ok) throw new Error(`KV write error: ${res.status}`); } export async function handler(request) { // Write a value (UTF-8 is preserved as-is) await kvPut("user/123", JSON.stringify({ name: "Alice 👋" })); // Read it back -> '{"name":"Alice 👋"}' const user = await kvGet("user/123"); return new Response(user); } ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "strings" ) var ( kvNamespaceID = os.Getenv("KV_MY_CACHE_ID") apiKey = os.Getenv("TELNYX_API_KEY") ) // keys may contain "/" as a separator; escape each segment individually func kvURL(key string) string { segs := strings.Split(key, "/") for i, s := range segs { segs[i] = url.PathEscape(s) } return fmt.Sprintf("https://api.telnyx.com/v2/storage/kvs/%s/keys/%s", kvNamespaceID, strings.Join(segs, "/")) } func kvGet(key string) (string, error) { req, _ := http.NewRequest("GET", kvURL(key), nil) req.Header.Set("Authorization", "Bearer "+apiKey) resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode == 404 { return "", nil // Key not found } if resp.StatusCode >= 400 { return "", fmt.Errorf("KV read error: %d", resp.StatusCode) } body, _ := io.ReadAll(resp.Body) return string(body), nil // Raw stored bytes } func kvPut(key, value string) error { req, _ := http.NewRequest("PUT", kvURL(key), strings.NewReader(value)) // verbatim req.Header.Set("Authorization", "Bearer "+apiKey) resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("KV write error: %d", resp.StatusCode) } return nil } func handler(w http.ResponseWriter, r *http.Request) { kvPut("user/123", `{"name": "Alice 👋"}`) user, _ := kvGet("user/123") w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, user) } ``` ```python import os from urllib.parse import quote import httpx KV_NAMESPACE_ID = os.getenv("KV_MY_CACHE_ID") API_KEY = os.getenv("TELNYX_API_KEY") BASE_URL = f"https://api.telnyx.com/v2/storage/kvs/{KV_NAMESPACE_ID}/keys" async def kv_get(key: str) -> str | None: async with httpx.AsyncClient() as client: # keep "/" literal so it stays a key separator response = await client.get( f"{BASE_URL}/{quote(key, safe='/')}", headers={"Authorization": f"Bearer {API_KEY}"} ) if response.status_code == 404: return None # Key not found response.raise_for_status() # Raise on 401, 429, 5xx, etc. return response.text # Raw stored bytes async def kv_put(key: str, value: str) -> None: async with httpx.AsyncClient() as client: response = await client.put( f"{BASE_URL}/{quote(key, safe='/')}", headers={"Authorization": f"Bearer {API_KEY}"}, content=value.encode() # Stored verbatim — no base64, no envelope ) response.raise_for_status() # Raise on non-2xx class Function: async def handler(self, request): await kv_put("user/123", '{"name": "Alice 👋"}') user = await kv_get("user/123") return {"body": user} ``` ```java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; public class KVClient { private final String namespaceId = System.getenv("KV_MY_CACHE_ID"); private final String apiKey = System.getenv("TELNYX_API_KEY"); private final HttpClient client = HttpClient.newHttpClient(); // Keys may contain "/" as a separator; encode each segment, keeping "/" literal. private String url(String key) { StringBuilder sb = new StringBuilder(); for (String seg : key.split("/", -1)) { if (sb.length() > 0) sb.append('/'); sb.append(URLEncoder.encode(seg, StandardCharsets.UTF_8).replace("+", "%20")); } return "https://api.telnyx.com/v2/storage/kvs/" + namespaceId + "/keys/" + sb; } /** Get a value. Returns null if the key does not exist. */ public String get(String key) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url(key))) .header("Authorization", "Bearer " + apiKey) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() == 404) { return null; // Key not found } if (response.statusCode() >= 400) { throw new RuntimeException("KV read error: " + response.statusCode()); } return response.body(); // Raw stored bytes } /** Put a value, stored verbatim (UTF-8). */ public void put(String key, String value) throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url(key))) .header("Authorization", "Bearer " + apiKey) .PUT(HttpRequest.BodyPublishers.ofString(value, StandardCharsets.UTF_8)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { throw new RuntimeException("KV write error: " + response.statusCode()); } } } ``` A native SDK is planned. Until it ships, use the HTTP endpoints shown above. ## Best Practices ### 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 ``` ### Value Serialization KV stores values verbatim, so serialize complex values yourself (no base64 needed): ```javascript // Write const value = JSON.stringify({ name: "Alice", age: 30 }); await kvPut("user/123", value); // Read const user = JSON.parse(await kvGet("user/123")); ``` ### Error Handling Handle missing keys gracefully: ```javascript const value = await kvGet("possibly-missing-key"); if (value === null) { // Key doesn't exist return new Response("Not found", { status: 404 }); } ``` --- ### KV API Reference > Source: https://developers.telnyx.com/docs/edge-compute/kv/api-reference.md Complete reference for the KV REST API. Base URL: `https://api.telnyx.com/v2/storage/kvs` ## Namespace Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | POST | `/v2/storage/kvs` | Create a new namespace | | GET | `/v2/storage/kvs` | List all namespaces | | GET | `/v2/storage/kvs/{id}` | Get namespace details | | DELETE | `/v2/storage/kvs/{id}` | Delete a namespace | ## Key-Value Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | PUT | `/v2/storage/kvs/{id}/keys/{key}` | Write a value | | GET | `/v2/storage/kvs/{id}/keys/{key}` | Read a value | | DELETE | `/v2/storage/kvs/{id}/keys/{key}` | Delete a key | | GET | `/v2/storage/kvs/{id}/keys` | List keys | --- ## Namespace Operations ### Create Namespace The `name` may contain only lowercase letters, numbers, and hyphens. ```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"}' ``` Response: ```json { "data": { "record_type": "storage_kv", "id": "550e8400-e29b-41d4-a716-446655440000", "name": "my-cache", "status": "pending", "created_at": "2024-01-15T10:30:00.000Z", "updated_at": "2024-01-15T10:30:00.000Z" } } ``` ### Namespace Status Poll the namespace until status is `provision_ok`: | Status | Description | |--------|-------------| | `pending` | Provisioning in progress | | `provision_ok` | Ready to use | | `provision_failed` | Provisioning failed | | `deleting` | Deletion in progress | | `delete_failed` | Deletion failed | | `deleted` | Fully removed (returns 404) | --- ## Key-Value Operations ### Keys A key is a path-like string. Allowed characters: `a-z`, `A-Z`, `0-9`, and `-` `_` `/` `=` `.`. Use `/` to group related keys (for example `user/123`). A key with any other character (such as `:`) is rejected with `400` (error code `10015`). ### Write a Value The request body **is** the value — KV stores the raw bytes you send, verbatim. There is no base64 encoding and no JSON envelope. ```bash curl -X PUT "https://api.telnyx.com/v2/storage/kvs/{id}/keys/my-key" \ -H "Authorization: Bearer $TELNYX_API_KEY" \ --data-binary "Hello World" ``` Returns `201` when the key is created and `200` when an existing key is updated; the response body is empty. KV has no server-side TTL or per-key metadata. To expire keys, store an expiry timestamp inside the value and check it on read — see [Key expiration](/docs/edge-compute/kv/ttl-and-metadata). ### Read a Value ```bash curl "https://api.telnyx.com/v2/storage/kvs/{id}/keys/my-key" \ -H "Authorization: Bearer $TELNYX_API_KEY" ``` The response body is the raw stored value (`Content-Type: application/octet-stream`): ``` Hello World ``` A missing key returns `404`. ### Delete a Key ```bash curl -X DELETE "https://api.telnyx.com/v2/storage/kvs/{id}/keys/my-key" \ -H "Authorization: Bearer $TELNYX_API_KEY" ``` ### List Keys ```bash curl "https://api.telnyx.com/v2/storage/kvs/{id}/keys?prefix=user/&limit=100" \ -H "Authorization: Bearer $TELNYX_API_KEY" ``` Query parameters: - `prefix` — Filter keys by prefix - `cursor` — Pagination cursor from a previous response's `meta.cursor` - `limit` — Maximum keys to return, `1`–`1000` (default `1000`) Response (keys only — list does not return 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`, the response includes `meta.cursor`; pass it back as the `cursor` query parameter to fetch the next page. --- ## Bindings Configuration Connect your function to a KV namespace using bindings in `func.toml`: ```toml [edge_compute] func_name = "my-function" [storage.kv.MY_CACHE] id = "550e8400-e29b-41d4-a716-446655440000" [storage.kv.SESSION_STORE] id = "660f9500-f39c-51e5-b817-557766551111" ``` The binding name (e.g., `MY_CACHE`) is surfaced to your function as an environment variable: - `KV_MY_CACHE_ID` — The namespace ID --- ### KV 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 # Delete a key telnyx-edge storage kv key delete user/123 ``` ### 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). KV has no native TTL or metadata; there are no `--ttl` or `--metadata` flags. See [Key expiration](/docs/edge-compute/kv/ttl-and-metadata) for an application-level expiry pattern. --- ### Key expiration > Source: https://developers.telnyx.com/docs/edge-compute/kv/ttl-and-metadata.md KV stores values as opaque bytes. It has **no server-side TTL and no per-key metadata** — a value is exactly the bytes you write, and it lives until you delete it. There are no `expiration_ttl`, `expiration`, or `metadata` fields on a write, and no `--ttl`/`--metadata` CLI flags. Anything you put in the request body (including JSON that looks like those fields) is stored verbatim as the value, not interpreted. ## Expiring keys at the application level To get expiry behavior, wrap your value with an `expires_at` timestamp and check it when you read. If it's in the past, treat the key as missing (and optionally delete it). ```javascript // Built on the raw kvGet/kvPut from the Quick Start. async function kvPutWithExpiry(key, value, ttlSeconds) { const envelope = JSON.stringify({ value, expires_at: Date.now() + ttlSeconds * 1000, }); await kvPut(key, envelope); } async function kvGetWithExpiry(key) { const raw = await kvGet(key); if (raw === null) return null; // key not found const { value, expires_at } = JSON.parse(raw); if (Date.now() > expires_at) { await kvDelete(key); // lazily clean up return null; // expired } return value; } // kvDelete helper async function kvDelete(key) { await fetch(`${BASE}/${encodeURIComponent(key)}`, { method: "DELETE", headers: { "Authorization": `Bearer ${API_KEY}` }, }); } ``` ```javascript // Usage: a session that "expires" after one hour await kvPutWithExpiry("session/abc", JSON.stringify({ userId: 42 }), 3600); const session = await kvGetWithExpiry("session/abc"); // null once an hour has passed ``` 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. Run a periodic sweep with [cron triggers](/docs/edge-compute/configuration/cron-triggers) if you need eager cleanup. - **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. --- ### KV Use Cases > Source: https://developers.telnyx.com/docs/edge-compute/kv/use-cases.md Patterns for using KV in your edge functions. These examples build on the raw `kvGet`/`kvPut` helpers from the [Quick Start](/docs/edge-compute/kv/quick-start) and the `kvGetWithExpiry`/`kvPutWithExpiry` helpers from [Key expiration](/docs/edge-compute/kv/ttl-and-metadata) (KV has no native TTL, so expiry is enforced in your code). ## Session Storage with Expiry Store user sessions at the edge with an application-level expiry: ```javascript const SESSION_TTL = 86400; // 24 hours async function handler(request) { const sessionId = request.headers.get("X-Session-ID"); // Read session (null if missing or expired) let session = await kvGetWithExpiry(`session/${sessionId}`); if (!session) { session = JSON.stringify({ created: Date.now(), views: 0 }); } else { const data = JSON.parse(session); data.views++; session = JSON.stringify(data); } // Store with a 24h expiry await kvPutWithExpiry(`session/${sessionId}`, session, SESSION_TTL); return new Response(session); } ``` ```python import json, time SESSION_TTL = 86400 # 24 hours # kv_get_with_expiry / kv_put_with_expiry: see Key expiration async def handler(request): session_id = request.headers.get("X-Session-ID") session = await kv_get_with_expiry(f"session/{session_id}") if not session: session = json.dumps({"created": int(time.time()), "views": 0}) else: data = json.loads(session) data["views"] += 1 session = json.dumps(data) await kv_put_with_expiry(f"session/{session_id}", session, SESSION_TTL) return {"body": session} ``` ```go const sessionTTL = 86400 // 24 hours // kvGetWithExpiry / kvPutWithExpiry: see Key expiration func handler(w http.ResponseWriter, r *http.Request) { sessionID := r.Header.Get("X-Session-ID") session, _ := kvGetWithExpiry(fmt.Sprintf("session/%s", sessionID)) var data map[string]interface{} if session == "" { data = map[string]interface{}{"created": time.Now().Unix(), "views": 0} } else { json.Unmarshal([]byte(session), &data) data["views"] = data["views"].(float64) + 1 } updated, _ := json.Marshal(data) kvPutWithExpiry(fmt.Sprintf("session/%s", sessionID), string(updated), sessionTTL) w.Header().Set("Content-Type", "application/json") w.Write(updated) } ``` ## API Response Caching with Expiry Cache expensive API responses with an application-level expiry: ```javascript const CACHE_TTL = 300; // 5 minutes async function handler(request) { const cacheKey = `cache/api${new URL(request.url).pathname}`; // Check cache const cached = await kvGetWithExpiry(cacheKey); if (cached) { return new Response(cached, { headers: { "X-Cache": "HIT" } }); } // Fetch from origin const response = await fetch("https://api.example.com/data"); const data = await response.text(); // Cache with a 5-minute expiry await kvPutWithExpiry(cacheKey, data, CACHE_TTL); return new Response(data, { headers: { "X-Cache": "MISS" } }); } ``` ```python CACHE_TTL = 300 # 5 minutes async def handler(request): from urllib.parse import urlparse cache_key = f"cache/api{urlparse(request.url).path}" cached = await kv_get_with_expiry(cache_key) if cached: return {"body": cached, "headers": {"X-Cache": "HIT"}} async with httpx.AsyncClient() as client: resp = await client.get("https://api.example.com/data") data = resp.text await kv_put_with_expiry(cache_key, data, CACHE_TTL) return {"body": data, "headers": {"X-Cache": "MISS"}} ``` ```go const cacheTTL = 300 // 5 minutes func handler(w http.ResponseWriter, r *http.Request) { cacheKey := fmt.Sprintf("cache/api%s", r.URL.Path) cached, _ := kvGetWithExpiry(cacheKey) if cached != "" { w.Header().Set("X-Cache", "HIT") w.Write([]byte(cached)) return } resp, _ := http.Get("https://api.example.com/data") defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) kvPutWithExpiry(cacheKey, string(data), cacheTTL) w.Header().Set("X-Cache", "MISS") w.Write(data) } ``` ## Feature Flags Store and retrieve feature flags (no expiry needed): ```javascript async function handler(request) { const newUIEnabled = await kvGet("flag/new-ui"); if (newUIEnabled === "true") { return serveNewUI(request); } return serveOldUI(request); } ``` ```python async def handler(request): enabled = await kv_get("flag/new-ui") if enabled == "true": return serve_new_ui(request) return serve_old_ui(request) ``` ```go func handler(w http.ResponseWriter, r *http.Request) { enabled, _ := kvGet("flag/new-ui") if enabled == "true" { serveNewUI(w, r) return } serveOldUI(w, r) } ``` --- ### KV 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. ---