# Telnyx Compute: Functions — Full Documentation > Complete page content for Functions (Compute section) of the Telnyx developer docs (https://developers.telnyx.com). > This file: https://developers.telnyx.com/docs/development/llms/compute-functions-llms-full-txt.md · Root index: https://developers.telnyx.com/llms.txt ## Overview ### Functions > Source: https://developers.telnyx.com/docs/edge-compute/overview.md A **function** is the compute primitive of Telnyx Edge Compute: an ordinary HTTP server, packaged as a container, deployed to Telnyx's global edge network, and served at its own public URL. You write a server that listens on `PORT`; one command builds and ships it. ```ts index.ts import * as http from "node:http"; const server = http.createServer((req, res) => { if (req.url === "/health") { res.writeHead(200); res.end(); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello from Telnyx Edge Compute!" })); }); server.listen(process.env.PORT || 8080); ``` ```bash telnyx-edge ship # 📡 Your function is live at: # https://demo-0198c2c5-8.telnyxcompute.com ``` On its own, a function just echoes HTTP. What makes it useful is the platform it plugs into — key-value storage, durable per-entity state, object storage, the Telnyx API, and private networking — each declared as a binding and reachable from your handler. This page maps that platform; the rest of these docs go deep on each piece. Install the CLI, scaffold a function, ship it, and curl the live URL — about five minutes. HTTP is the only trigger today — there are no cron triggers. For scheduled work, call the function's URL from an external scheduler, such as a GitHub Actions cron job. ## The platform around your functions Declare a binding in `func.toml` and it surfaces on `env` at runtime in TypeScript, or as REST and injected environment variables in every other language. See [Bindings](/docs/edge-compute/runtime/bindings) for the mechanics — these are the products worth reaching for. **State and storage** Globally distributed key-value storage with server-side TTL — an `env` binding in TypeScript, REST everywhere else. For caches, sessions, and feature flags. Beta — durable per-entity state and coordination: one instance per name, one call at a time. For counters, per-user state, and anything that needs a serialized owner. S3-compatible buckets for files and media — a separate product, reached over its S3 API from any language. **Connect to Telnyx** A pre-authenticated client for Voice, Messaging, and AI — or plain REST with the injected key. Answer calls, send messages, and run inference without managing credentials. ## Languages `telnyx-edge new-func -l ` scaffolds a project in TypeScript (`ts`), JavaScript (`js`), Go (`go`), Python (`python`), or Java (`quarkus`). Each language uses its own standard server contract — `node:http`, Go's `http.Handler`, ASGI, Quarkus Funqy — documented in [HTTP handler](/docs/edge-compute/runtime/http-handler). The binding SDK (`@telnyx/edge-runtime`) — a typed `env` exposing a pre-authenticated Telnyx client, secrets, and KV namespaces — is TypeScript-only today. The other runtimes use the same features over REST and injected environment variables. ## Where your code runs Functions run on the infrastructure that carries Telnyx voice and messaging traffic: edge sites inside carrier facilities, close to end users rather than in generic cloud availability zones. Each region is backed by multiple independent sites across different providers, with automatic failover if a site goes down. ## Resources The entrypoint contract for each language. Environment variables, secrets, routing, and versions. Every `telnyx-edge` command and flag. Request timeouts, payload sizes, and quotas. Free tier plus usage-based rates for requests and CPU time. Ask questions and share what you build. --- ## Get Started ### Quickstart > Source: https://developers.telnyx.com/docs/edge-compute/quickstart.md Deploy your first function end-to-end: install the CLI, authenticate, scaffold, ship, and prove it with `curl`. A function is a container running your own HTTP server on Telnyx infrastructure, reachable at a public URL. The code steps below are shown for every supported language — pick a tab. TypeScript is the default and the only one with the typed binding SDK today; the rest are fully supported for plain HTTP. ## Prerequisites - A Telnyx account — [sign up](https://telnyx.com/sign-up) if you don't have one. - The toolchain for your language: Node.js ≥ 18 (TypeScript/JavaScript), Go, Python 3, or Java with Maven. ## 1. Install the CLI The `telnyx-edge` CLI ships as binaries on the [GitHub releases page](https://github.com/team-telnyx/edge-compute/releases). Assets are version-stamped, and each tarball extracts into a versioned directory containing the binary. ```bash Linux (amd64) curl -fsSL https://github.com/team-telnyx/edge-compute/releases/download/v0.2.3/telnyx-edge-v0.2.3-linux-amd64.tar.gz | tar xz sudo mv telnyx-edge-v0.2.3-linux-amd64/telnyx-edge /usr/local/bin/ ``` ```bash macOS (Apple silicon) curl -fsSL https://github.com/team-telnyx/edge-compute/releases/download/v0.2.3/telnyx-edge-v0.2.3-macos-arm64.tar.gz | tar xz sudo mv telnyx-edge-v0.2.3-macos-arm64/telnyx-edge /usr/local/bin/ ``` For Intel Macs use the `macos-amd64` asset; Windows zips are on the same releases page. Verify the install: ```bash telnyx-edge --version ``` ## 2. Authenticate Authenticate before creating a function — `new-func` registers the function with the platform, which requires credentials. ```bash # Interactive: opens your browser for OAuth telnyx-edge auth login # Or headless (CI, containers): set an API key directly telnyx-edge auth api-key set # Confirm telnyx-edge auth status ``` Credentials persist to `~/.telnyx-edge/config.toml`. The CLI does not read a `TELNYX_API_KEY` environment variable — in CI, run `auth api-key set` as a setup step. ## 3. Create a function `new-func` creates the function server-side, writes the assigned UUID into `func.toml`, and scaffolds a working project. Pass `-l` to pick the language: ```bash TypeScript telnyx-edge new-func -l ts -n hello cd hello npm install ``` ```bash JavaScript telnyx-edge new-func -l js -n hello cd hello npm install ``` ```bash Go telnyx-edge new-func -l go -n hello cd hello ``` ```bash Python telnyx-edge new-func -l python -n hello cd hello ``` ```bash Java telnyx-edge new-func -l quarkus -n hello cd hello ``` Every scaffold contains a `func.toml` that ties the directory to the registered function: ```toml [edge_compute] func_id = "7819cf01-39a8-400e-9bce-3d792ffa4017" # assigned by new-func func_name = "hello" ``` The entrypoint file and its contract differ by language — TypeScript and JavaScript run their own server; Go, Python, and Java hand you a handler and run the server for you. Here is the scaffolded entrypoint, condensed to a health check plus a default JSON response: ```ts index.ts import * as http from "node:http"; const server = http.createServer((req, res) => { // Liveness/readiness probes — answer first if (req.url === "/health" || req.url?.startsWith("/health/")) { res.writeHead(200); res.end(); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello from Telnyx Edge Compute!" })); }); server.listen(process.env.PORT || 8080); ``` ```js index.js import * as http from "node:http"; const server = http.createServer((req, res) => { // Liveness/readiness probes — answer first if (req.url === "/health" || req.url?.startsWith("/health/")) { res.writeHead(200); res.end(); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello from Telnyx Edge Compute!" })); }); server.listen(process.env.PORT || 8080); ``` ```go handler.go package function import ( "encoding/json" "net/http" ) // The platform runs the server and calls Handle per request. func Handle(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "message": "Hello from Telnyx Edge Compute!", }) } ``` ```python function/func.py import json def new(): return Function() class Function: async def handle(self, scope, receive, send): body = json.dumps({"message": "Hello from Telnyx Edge Compute!"}).encode() await send({"type": "http.response.start", "status": 200, "headers": [[b"content-type", b"application/json"]]}) await send({"type": "http.response.body", "body": body}) ``` ```java src/main/java/functions/Function.java package functions; import io.quarkus.funqy.Funq; public class Function { @Funq public Output function(Input input) { return new Output("Hello from Telnyx Edge Compute!"); } } ``` These are trimmed for the quickstart. [HTTP handler](/docs/edge-compute/runtime/http-handler) has the full scaffold for each language, the exact entrypoint contract, and how request bodies and health probes work. ## 4. Ship ```bash telnyx-edge ship ``` `ship` uploads the project, builds it, deploys it, and monitors the rollout (default timeout 5 minutes; `--timeout` to change). The output ends with the live URL: ``` ✅ Func 'hello' is now deployed! 📡 Your function is live at: https://hello-.telnyxcompute.com 💡 Test your function: curl https://hello-.telnyxcompute.com ``` Every function gets a URL of the form `{func-name}-{func-id-prefix}.telnyxcompute.com` — see [Routes & Domains](/docs/edge-compute/configuration/routing). ## 5. Call it Use the URL `ship` printed. Every scaffold answers a `GET` with the same default response: ```bash curl -sS https://hello-.telnyxcompute.com # → {"message":"Hello from Telnyx Edge Compute!"} ``` Sending request bodies differs by contract — the TypeScript, JavaScript, Go, and Python scaffolds read the raw body, while the Java (Funqy) scaffold is JSON-in, JSON-out. See [HTTP handler](/docs/edge-compute/runtime/http-handler) for each. The function is live. Iterate by editing the entrypoint and running `telnyx-edge ship` again — each successful ship creates an immutable revision you can [roll back to](/docs/edge-compute/configuration/versions). ## Next Steps - [Bindings](/docs/edge-compute/runtime/bindings) — pre-authenticated Telnyx client, secrets, and KV on `env`. - [KV quick start](/docs/edge-compute/kv/quick-start) — persist data across requests from your function. --- ## Best Practices ### Best Practices > Source: https://developers.telnyx.com/docs/edge-compute/best-practices.md An Edge Compute function is a real container running your own HTTP server — not a per-request sandbox. Most of the practices below follow from that model: module scope runs once per container, an escaped exception kills a process, and state has to live somewhere other than the container. Code samples are TypeScript. The same principles apply in every runtime; the binding SDK (`@telnyx/edge-runtime`) is TypeScript-only today, so other languages use environment variables and the REST APIs where a binding is shown. ## Configuration ### Keep Secrets Out of Code Store credentials as secrets — the CLI takes the key and value as positional arguments: ```bash telnyx-edge secrets add API_KEY "sk-..." ``` Every secret is injected into your functions as a plain environment variable, so this works in any language: ```ts const apiKey = process.env.API_KEY; ``` TypeScript projects that declare a `[[secrets]]` binding in `func.toml` can also read it through `env.SECRETS.get("")`, which `telnyx-edge types` type-checks against the declared handles. Both surfaces are live at the same time — see [Secrets](/docs/edge-compute/configuration/secrets). ### Budget for the Platform Timeout The request timeout is **30 seconds by default and 60 seconds at most** — there is no `func.toml` field that raises it. A request that exceeds it is terminated with a `504`. Set your own deadlines on outbound calls a few seconds below the platform's so you fail with a useful error instead (see [Time Out and Retry Outbound Calls](#time-out-and-retry-outbound-calls)), and split work that genuinely needs longer. Exact numbers: [Limits](/docs/edge-compute/platform/limits). ### Name Functions for Their URL The function name becomes the hostname — `{func-name}-{func-id-prefix}.telnyxcompute.com` — and `new-func` registers the function with the platform at scaffold time, so pick the name up front: ```bash # Good — the URL says what it serves telnyx-edge new-func -l ts -n user-api # Bad — you'll be curling https://test-yourorg.telnyxcompute.com in production telnyx-edge new-func -l ts -n test ``` ## Performance ### Initialize Once, at Module Scope A container serves many requests. Module scope runs once per container; the request callback runs per request. Build clients, load config, and compile anything expensive outside the callback: ```ts import * as http from "node:http"; // Once per container — reused across requests const client = new SomeApiClient({ timeout: 5000 }); const server = http.createServer(async (req, res) => { // Per request — keep construction out of here const data = await client.get("/data"); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(data)); }); server.listen(process.env.PORT || 8080); ``` The same rule holds everywhere: package-level variables in Go, module-level objects in Python. ### Keep Cold Starts Small A new container starts when traffic scales up or after a deploy, and the first request it serves waits for everything before `server.listen` — imports, client construction, config loads. Keep dependencies minimal, and lazy-load heavy libraries used only on rare paths so the common path doesn't pay for them. ### Cache Expensive Reads in KV Declare a KV namespace in `func.toml` and it resolves as a binding on `env`: ```toml # func.toml [storage.kv.CACHE] id = "550e8400-e29b-41d4-a716-446655440000" # from `telnyx-edge storage kv create` ``` ```ts import { env } from "@telnyx/edge-runtime"; async function getUser(userId: string) { const cached = await env.CACHE.get(`user/${userId}`, { type: "json" }); if (cached) return cached; const user = await fetchUserFromDb(userId); await env.CACHE.put(`user/${userId}`, JSON.stringify(user), { expirationTtl: 300 }); // seconds return user; } ``` Two contracts to know: - `expirationTtl` is server-side expiry in whole seconds (≥ 1) and requires `@telnyx/edge-runtime` **≥ 0.2.2** — earlier versions accept the option and silently ignore it. - Keys allow `a-z` `A-Z` `0-9` `-` `_` `/` `=` `.` and **forbid colons** — write `user/123`, not `user:123`. More in [KV Best Practices](/docs/edge-compute/kv/best-practices). ## Put State Where It Belongs Containers come and go, and concurrent requests can land on different containers — anything kept in process memory is a cache at best. Pick the store by the shape of the data: | Data | Use | Why | |------|-----|-----| | Per-entity state, counters, coordination | [Stateful Actors](/docs/edge-compute/stateful-actors) | One instance per name, one call at a time — read-modify-write without races | | Caches, config, feature flags | [KV](/docs/edge-compute/kv) | Small values (≤ 1 MiB), fast reads, last-write-wins | | Files and large blobs | [Storage buckets](/docs/cloud-storage/quick-start) | S3-compatible object storage | Don't build counters, locks, or rate limiters on KV: it has no transactions or compare-and-swap, and concurrent writers to one key are last-write-wins. That job is exactly what [Stateful Actors](/docs/edge-compute/stateful-actors) exist for. Full comparison: [Where state lives](/docs/edge-compute/runtime/execution-model#where-state-lives). ## Reliability ### Make Handlers Idempotent Clients retry and webhooks are redelivered, so design handlers where processing the same request twice has the same effect as once. Key side effects on a caller-supplied identifier — a webhook event id, an `Idempotency-Key` header — and skip work already done. If the duplicate check itself must be race-free, do it inside a [Stateful Actor](/docs/edge-compute/stateful-actors); a check-then-act on KV can race. ### Keep the Health Endpoint Fast The scaffolds answer `/health` before any other routing: ```ts if (req.url === "/health" || req.url?.startsWith("/health/")) { res.writeHead(200); res.end(); return; } ``` Keep that property: return immediately and never call a dependency from it, so a slow upstream can't make your function look down. ### Catch Everything at the Top of the Handler Your function is one process. An exception that escapes the request callback — including an unhandled promise rejection — crashes it, drops every in-flight request, and makes the next request pay a cold start. Wrap the whole handler body: ```ts const server = http.createServer(async (req, res) => { try { // ... handle the request } catch (err) { console.error("request failed:", err); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "internal error" })); } }); ``` ### Time Out and Retry Outbound Calls Don't let a slow upstream ride you into the platform's 30-second `504` — set an explicit deadline on every outbound call: ```ts const response = await fetch("https://api.example.com/data", { signal: AbortSignal.timeout(5000), }); ``` Retry only network failures and `5xx` responses, with exponential backoff, and keep the total budget under the platform timeout: ```ts async function fetchWithRetry(url: string, attempts = 3): Promise { for (let i = 0; ; i++) { try { const res = await fetch(url, { signal: AbortSignal.timeout(5000) }); if (res.status < 500) return res; // success or client error — don't retry 4xx if (i === attempts - 1) return res; } catch (err) { if (i === attempts - 1) throw err; } await new Promise((r) => setTimeout(r, 2 ** i * 100)); // 100 ms, 200 ms, 400 ms } } ``` ## Security - **Validate input before use** — check required fields and types, reject with `400`. Nothing between the internet and your handler does it for you. - **HTTPS only** for outbound calls. - **Never log secret values** — and don't log full request bodies, which may carry PII. Log metadata: method, path, status, duration. - **Authenticate anything that mutates state** — your function URL is public. Require a token or shared secret (stored as a [secret](/docs/edge-compute/configuration/secrets), checked in the handler) before acting on a request. ## Observability There is no logs command and no metrics dashboard today. What you have is the output of `ship`, `status`, and `inspect` — plus whatever your function emits itself. So emit deliberately: - **Send structured events over HTTP to a sink you control** (a log aggregator, your own collector) if you need request-level visibility. There is no surface that shows you `console.log` output. - **Propagate a request id** — read `X-Request-ID` or generate one, return it in the response, and attach it to every event you emit, so a user-reported failure is findable in your sink. Patterns and sink examples: [Observability](/docs/edge-compute/observability). ## Next Steps - [Limits](/docs/edge-compute/platform/limits) — the exact numbers behind timeouts, memory, and payload sizes - [Observability](/docs/edge-compute/observability) — building your own telemetry with no platform logging surface - [Execution Model](/docs/edge-compute/runtime/execution-model) — container lifecycle, scaling, and cold starts --- ## Guides ### AI Assistants and Edge Compute > Source: https://developers.telnyx.com/docs/edge-compute/guides/ai-assistant-backend.md Telnyx AI Assistants can call out to your own backend in several scenarios — resolving dynamic variables at the start of a conversation, executing webhook tool calls mid-conversation, and more. Whenever you need a backend for these callbacks, Telnyx Edge Compute is a natural fit: no server to manage, secrets injected at runtime, and deployment via a single CLI command. This guide walks through building a single Go function that handles both dynamic variables and webhook tool calls, using the demo app `telnyx-ai-edge` as the reference implementation. --- ## What you'll build A support assistant for "Telnyx Logistics" that: - Greets callers by name (dynamic variables resolved from the caller's phone number) - Has a `lookup-order` tool the assistant can call to retrieve order status, carrier, and estimated delivery Both the dynamic variable lookup and the tool call hit one Edge Compute function at a single URL. --- ## Prerequisites - A Telnyx account with [Edge Compute](/docs/edge-compute/quickstart) enabled. - The `telnyx-edge` CLI [installed and authenticated](/docs/edge-compute/quickstart). - An existing [AI Assistant](https://portal.telnyx.com/#/ai/assistants) (or you can create one via API as shown below). - Go 1.24+ installed locally (if following along with the Go sample). --- ## Key concepts ### Single function, two callbacks Edge Compute routes all HTTP methods and paths under your function URL to your handler — path handling is up to your code (see [Routes & Domains](/docs/edge-compute/configuration/routing)). The platform handles `/health/liveness` and `/health/readiness` probes automatically. In this guide, both the dynamic variables webhook and the webhook tool call point to the same function URL, so the handler dispatches on the **request body shape** rather than the URL path: - **Dynamic variables webhook** — Telnyx wraps the payload under `data.event_type`. - **Webhook tool call** — the body is the flat arguments object from the tool's `body_parameters` schema (e.g. `{"order_id": "ORD-10042"}`). You could also use separate paths (e.g. `/dynamic-variables` and `/tool/lookup-order`) if you prefer path-based routing — both approaches work. This guide uses body-shape dispatch to keep everything at a single URL. ### Webhook signature verification Telnyx signs every dynamic-variables webhook and webhook tool call with an Ed25519 key. The signature is in the `telnyx-signature-ed25519` header, and the timestamp is in `telnyx-timestamp`. The signed message is `"{timestamp}|{raw_body}"`. You must verify this signature to confirm the request is genuinely from Telnyx. Your org's public key is available at: ``` GET https://api.telnyx.com/v2/public_key Authorization: Bearer ``` The response contains `data.public` (not `data.public_key`) — the base64-encoded Ed25519 public key. ### Dynamic variables response format The response **must** nest variables under a `dynamic_variables` key. A flat object (e.g. `{"customer_name": "James"}`) is silently ignored — variables will remain unresolved. ```json { "dynamic_variables": { "customer_name": "James Smith", "account_tier": "premium" } } ``` ### Timeout The default dynamic variables webhook timeout is 1,500 ms. Edge Compute functions may occasionally need more time on a cold start, so consider setting `dynamic_variables_webhook_timeout_ms` on the assistant to a higher value (up to 10,000 ms). A value of 8,000 ms is a reasonable choice for edge backends. --- ## Step 1: Scaffold the function ```bash telnyx-edge new-func -l go -n telnyx-ai-edge cd telnyx-ai-edge ``` This creates a `func.toml` with the registered function ID and a Go handler scaffold. The Go module **must** be named `function` (package `function`, entrypoint `Handle(w, r)`). Other module names fail to build: "malformed module path: missing dot in first path element." Use `go 1.24` in `go.mod`. --- ## Step 2: Store the public key as a secret Fetch your org's public key and store it as an encrypted secret. The public key endpoint requires authentication — use your Telnyx API key: ```bash # Get the public key (requires authentication) PUBLIC_KEY=$(curl -s -H "Authorization: Bearer $TELNYX_API_KEY" \ https://api.telnyx.com/v2/public_key | jq -r '.data.public') # Store it as a secret (encrypted, org-scoped, injected as env var at runtime) telnyx-edge secrets add TELNYX_PUBLIC_KEY "$PUBLIC_KEY" ``` The function reads this secret from `os.Getenv("TELNYX_PUBLIC_KEY")` at startup. Secrets are never visible in `secrets list` — only the name is shown. --- ## Step 3: Write the handler The handler does three things: 1. Verifies the Telnyx Ed25519 signature on every request 2. Detects whether the request is a dynamic-variables webhook or a tool call 3. Returns the appropriate response ```go handler.go package function import ( "crypto/ed25519" "encoding/base64" "encoding/json" "io" "log" "net/http" "os" "strconv" "time" ) const maxSkew = 5 * time.Minute var publicKey ed25519.PublicKey func init() { raw := os.Getenv("TELNYX_PUBLIC_KEY") if raw == "" { log.Println("warning: TELNYX_PUBLIC_KEY is not set; all requests will be rejected") return } key, err := base64.StdEncoding.DecodeString(raw) if err != nil || len(key) != ed25519.PublicKeySize { log.Printf("warning: TELNYX_PUBLIC_KEY is invalid (len=%d, err=%v)", len(key), err) return } publicKey = ed25519.PublicKey(key) } func Handle(w http.ResponseWriter, r *http.Request) { // Health probes are handled by the platform — don't add custom health routes if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "cannot read body", http.StatusBadRequest) return } if !verifyTelnyxSignature(r.Header, body) { http.Error(w, "invalid signature", http.StatusForbidden) return } // Dispatch on body shape: DV webhook has "data.event_type", // tool call is a flat args object if isDynamicVariablesRequest(body) { handleDynamicVariables(w, body) return } handleLookupOrder(w, body) } func isDynamicVariablesRequest(body []byte) bool { var probe struct { Data *struct { EventType string `json:"event_type"` } `json:"data"` } if err := json.Unmarshal(body, &probe); err != nil { return false } return probe.Data != nil } func verifyTelnyxSignature(h http.Header, body []byte) bool { if publicKey == nil { return false } sig := h.Get("telnyx-signature-ed25519") ts := h.Get("telnyx-timestamp") if sig == "" || ts == "" { return false } t, err := strconv.ParseInt(ts, 10, 64) if err != nil { return false } age := time.Since(time.Unix(t, 0)) if age < -maxSkew || age > maxSkew { return false } s, err := base64.StdEncoding.DecodeString(sig) if err != nil { return false } signed := append([]byte(ts+"|"), body...) return ed25519.Verify(publicKey, signed, s) } // --- Dynamic Variables --- type dvRequest struct { Data struct { EventType string `json:"event_type"` Payload struct { Channel string `json:"telnyx_conversation_channel"` AgentTarget string `json:"telnyx_agent_target"` EndUserTarget string `json:"telnyx_end_user_target"` CallControlID string `json:"call_control_id"` AssistantID string `json:"assistant_id"` } `json:"payload"` } `json:"data"` } type dvResponse struct { DynamicVariables map[string]any `json:"dynamic_variables"` } func handleDynamicVariables(w http.ResponseWriter, body []byte) { var req dvRequest if err := json.Unmarshal(body, &req); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } caller := req.Data.Payload.EndUserTarget resp := dvResponse{ DynamicVariables: map[string]any{ "customer_name": lookupCustomerName(caller), "account_tier": "premium", "open_order_id": "ORD-10042", "support_region": "US", }, } writeJSON(w, resp) } func lookupCustomerName(caller string) string { known := map[string]string{ "+13128675309": "James Smith", "+15551234567": "Rachel Thomas", } if name, ok := known[caller]; ok { return name } return "there" } // --- Webhook Tool: lookup-order --- type toolRequest struct { OrderID string `json:"order_id"` } type toolResponse struct { OrderID string `json:"order_id"` Status string `json:"status"` EstimatedDeliv string `json:"estimated_delivery"` Carrier string `json:"carrier"` } func handleLookupOrder(w http.ResponseWriter, body []byte) { var req toolRequest if err := json.Unmarshal(body, &req); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } resp := toolResponse{ OrderID: req.OrderID, Status: "shipped", EstimatedDeliv: "2025-04-10", Carrier: "Telnyx Logistics", } writeJSON(w, resp) } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) } ``` --- ## Step 4: Ship the function ```bash telnyx-edge ship ``` The ship process takes 2–3 minutes. After `uploaded successfully`, poll `telnyx-edge list` until the status shows `deploy_ok`. Don't trust a CLI timeout as a failure — the function may still be building server-side. ```bash telnyx-edge list # FUNC ID FUNCTION NAME STATUS INVOKE URL # 917210e7-... telnyx-ai-edge deploy_ok https://telnyx-ai-edge-.telnyxcompute.com ``` Save the invoke URL — you'll point the assistant at it next. --- ## Step 5: Configure the AI Assistant Set the function URL as both the dynamic variables webhook URL and the webhook tool URL on the assistant. ### Dynamic variables webhook In the [Portal](https://portal.telnyx.com/#/ai/assistants) or via the API: | Field | Value | |-------|-------| | `dynamic_variables_webhook_url` | `https://telnyx-ai-edge-.telnyxcompute.com/` | | `dynamic_variables_webhook_timeout_ms` | `8000` | Consider setting the timeout to 8,000 ms to give the function room on cold starts. The default 1,500 ms may be tight for a cold function. ### Template variables in the assistant Use `{{variable_name}}` in the assistant's instructions and greeting to reference the variables your function returns: ``` instructions: "You are a support agent for Telnyx Logistics. The caller is {{customer_name}} (tier: {{account_tier}}). They may have open order {{open_order_id}}." greeting: "Hi {{customer_name}}, thanks for calling Telnyx Logistics. How can I help you today?" ``` ### Webhook tool Add a webhook tool that points to the same function URL: ```json { "type": "webhook", "webhook": { "name": "lookup-order", "description": "Look up the current status of a customer order by its order id.", "url": "https://telnyx-ai-edge-.telnyxcompute.com/", "method": "POST", "body_parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "The order id to look up, e.g. ORD-10042." } }, "required": ["order_id"] } } } ``` When the LLM decides to call `lookup-order`, Telnyx sends a POST with the tool arguments as the flat body (`{"order_id": "ORD-10042"}`), signed with the same Ed25519 key. Your function detects the body shape, handles it as a tool call, and returns the result. --- ## Step 6: Test end-to-end 1. **Call the function directly** (without a signature — it'll return 403, confirming it's live): ```bash curl -X POST https://telnyx-ai-edge-.telnyxcompute.com/ \ -H "Content-Type: application/json" \ -d '{"order_id":"ORD-10042"}' # → 403 invalid signature ← expected, signature verification is working ``` 2. **Make a test call** to the assistant from the Portal or via the API: ```bash curl --request POST \ --url https://api.telnyx.com/v2/texml/ai_calls/ \ --header "Authorization: Bearer $TELNYX_API_KEY" \ --header 'Content-Type: application/json' \ --data '{ "From": "+13128675309", "To": "+15551234567", "AIAssistantId": "assistant-" }' ``` 3. **Verify in the conversation transcript** that: - The greeting includes the resolved `customer_name` - The assistant can call `lookup-order` and read back real order data --- ## Tips and gotchas ### Choosing body-shape vs path-based dispatch Since Edge Compute routes all paths to your handler, you can use path-based routing (e.g. `r.URL.Path == "/tool/lookup-order"`) or body-shape dispatch as shown in this guide. Both work. If you configure separate URLs for the DV webhook and the tool on the assistant, path-based routing is natural. If you point both at the same URL, body-shape dispatch is the way to go. For a path-based routing example, see the [RESTful API example](https://github.com/team-telnyx/edge-compute-cli/tree/main/docs/examples/python/restful-api) in the Edge Compute CLI repo. ### Consider a higher webhook timeout The default dynamic variables webhook timeout is 1,500 ms. Edge Compute functions may occasionally need a bit more time on a cold start, so consider setting `dynamic_variables_webhook_timeout_ms` to 8,000 ms to give the function room. The maximum is 10,000 ms. ### Always verify signatures Without signature verification, anyone who knows your function URL can inject fake dynamic variables or tool responses. The `telnyx-signature-ed25519` and `telnyx-timestamp` headers are present on every request from Telnyx. ### Ship takes a few minutes A normal ship takes 2–3 minutes. The CLI's build monitor has a 5-minute timeout, but the build continues server-side regardless. If the CLI reports a timeout, check `telnyx-edge list` for the actual status before retrying — the function may have deployed successfully. ### Secrets require re-shipping Adding or changing a secret (`telnyx-edge secrets add`) does not affect an already-deployed function. Run `telnyx-edge ship` again to pick up the new secret. ### The `dynamic_variables` wrapper is mandatory Returning a flat JSON object like `{"customer_name": "James"}` will be silently ignored. Variables must be nested under `dynamic_variables`: ```json { "dynamic_variables": { "customer_name": "James" } } ``` --- ## Next steps - [Dynamic Variables](/docs/inference/ai-assistants/dynamic-variables) — full reference for the DV webhook payload and resolution precedence. - [Webhook signing](/docs/development/api-fundamentals/webhooks/receiving-webhooks#webhook-signing) — how Telnyx signs webhooks and how to verify signatures. - [Edge Compute quickstart](/docs/edge-compute/quickstart) — getting started with your first function. - [Secrets](/docs/edge-compute/configuration/secrets) — encrypted, org-scoped environment variables. - [Bindings](/docs/edge-compute/runtime/bindings) — pre-authenticated Telnyx API client for your function. --- ## Local Development ### Local Development > Source: https://developers.telnyx.com/docs/edge-compute/development.md An Edge Compute function is an ordinary program in a container, not code inside a proprietary runtime. The TypeScript and JavaScript scaffolds run their own HTTP server on `$PORT` (default 8080); the Python, Go, and Quarkus scaffolds export a handler and the server is run for you. Local development is therefore unremarkable: run the program, `curl localhost:8080`, iterate, then `telnyx-edge ship`. Everything below runs against the projects generated by `telnyx-edge new-func` (see the [Quickstart](/docs/edge-compute/quickstart)); the Python and Go *serve* rows add a small local entry point shown in their tabs: | Language | Serve locally | Test | |----------|---------------|------| | TypeScript | `npm run build && npm start` | `node --test` | | JavaScript | `node index.js` | `node --test` | | Python | `uvicorn app:app --port 8080 --interface asgi3 --lifespan off` | `pytest` | | Go | `go run ./cmd/local` (scratch `main`) | `go test ./...` | | Java (Quarkus) | `./mvnw quarkus:dev` | `./mvnw test` | ## Run and test, by language The scaffold's `index.ts` (or `index.js`) at the project root is a `node:http` server. Build (TypeScript only) and run it: ```bash npm install npm run build # tsc → dist/index.js npm start # node dist/index.js — for JavaScript: node index.js # → Server running on port 8080 ``` `npm run dev` runs `index.ts` directly through `ts-node` with no build step. The server reads `process.env.PORT` and falls back to 8080. Exercise it with curl: ```bash curl localhost:8080/ # → {"message":"Hello from Telnyx Edge Compute!"} curl -X POST localhost:8080/ -H "Content-Type: application/json" -d '{"name":"test"}' # → {"message":"Hello from Telnyx Edge Compute!","data":{"name":"test"}} curl -s -o /dev/null -w "%{http_code}\n" localhost:8080/health # → 200 ``` The `/health` fast-path at the top of the scaffold answers the platform's probes — keep it fast and dependency-free when you rework the server. The scaffold starts its server at module load and exports nothing, so there is nothing to import in a unit test. Keep request handling in exported functions the server delegates to, test those with `node --test`, and treat curl against the running server as your integration test. The scaffold is an ASGI application: `function/func.py` exposes a module-level `new()` factory, and the returned instance's `handle(scope, receive, send)` method is the ASGI callable. Any ASGI server runs it — with uvicorn, add a local entry point: ```python # app.py — local entry point, not part of the function contract from function import new app = new().handle ``` ```bash pip install uvicorn uvicorn app:app --port 8080 --interface asgi3 --lifespan off # → Uvicorn running on http://127.0.0.1:8080 ``` `--interface asgi3` is required — `handle` is a bound method and uvicorn's interface auto-detection misclassifies it, returning 500s without the flag. `--lifespan off` silences a harmless `Invalid ASGI scope type: lifespan` error at startup — the scaffold doesn't implement the lifespan protocol uvicorn probes for by default. uvicorn also never calls the optional `start(cfg)`/`stop()` hooks; only the platform does. For tests, the scaffold's `pyproject.toml` already declares `pytest`, `pytest-asyncio`, and `httpx` as dev dependencies, with `asyncio_mode = "strict"` — mark async tests explicitly. No server is needed: call `handle` directly with a hand-built scope. ```bash pip install -e ".[dev]" ``` ```python # tests/test_handle.py import json import pytest from function import new @pytest.mark.asyncio async def test_get_returns_greeting(): func = new() sent = [] async def receive(): return {"type": "http.request", "body": b""} async def send(message): sent.append(message) scope = {"type": "http", "method": "GET", "path": "/", "headers": []} await func.handle(scope, receive, send) assert sent[0]["type"] == "http.response.start" assert sent[0]["status"] == 200 assert json.loads(sent[1]["body"])["message"] == "Hello from Telnyx Edge Compute!" ``` ```bash pytest # → 1 passed ``` The scaffold exports `Handle(w http.ResponseWriter, r *http.Request)` from `package function` — there is no `main()`; the platform provides the server. `net/http/httptest` drives `Handle` without one: ```go // handler_test.go package function import ( "net/http" "net/http/httptest" "strings" "testing" ) func TestHandlePostEchoesJSON(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"name":"test"}`)) rec := httptest.NewRecorder() Handle(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d", rec.Code) } if !strings.Contains(rec.Body.String(), `"name":"test"`) { t.Fatalf("body did not echo request: %s", rec.Body.String()) } } ``` ```bash go test ./... # → ok function 0.6s ``` For a curl-able local server, add a scratch `main` in its own package. The import path is `function` because that is the module path in the scaffold's `go.mod`: ```go // cmd/local/main.go — local harness only package main import ( "log" "net/http" "function" ) func main() { http.HandleFunc("/", function.Handle) log.Println("listening on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) } ``` ```bash go run ./cmd/local curl localhost:8080/ # → {"message":"Hello from Telnyx Edge Compute!"} ``` The scaffold is a Quarkus Funqy project. Dev mode gives hot reload on source changes and a debugger on port 5005: ```bash ./mvnw quarkus:dev ``` ```bash curl localhost:8080/ -H "Content-Type: application/json" -d '{"message":"ping"}' # → {"message":"ping"} ``` The test stack (`quarkus-junit5`, `rest-assured`) is already in the `pom.xml`: ```bash ./mvnw test ``` ## Testing A standalone function needs no emulator: the platform runs your program in a container and sends it HTTP, which is exactly what your terminal just did — the language-standard tools above are the whole testing story. (Projects that add [Stateful Actors](/docs/edge-compute/stateful-actors) run locally with `telnyx-edge dev`; see its docs.) The one structural habit worth adopting: keep transport separate from logic. Parse, validate, and compute in plain functions; let the server (or `Handle`, or `handle`) stay a thin adapter. That keeps unit tests fast and keeps the binding limitation below out of most of your test suite. ## Framework servers For TypeScript and JavaScript, the platform contract is a process that binds the port in `$PORT` (default 8080). Any HTTP server that does so runs in the container — Express, Fastify, Hono, or anything else — and the server you ran locally is the server that runs deployed. There are no framework-specific guides. ## Bindings and secrets The [`env` binding surface](/docs/edge-compute/runtime/bindings) — `[telnyx]` clients, `env.SECRETS.get()`, KV namespaces, actors — resolves only inside a deployed function. A standalone function has no local binding emulation — to exercise binding-backed code paths, ship to a scratch function and curl its live URL. ([Stateful Actors](/docs/edge-compute/stateful-actors) projects are the exception: `telnyx-edge dev` runs their actor stack locally so `env.` calls resolve.) Plain environment variables are the exception. In production, secrets created with `telnyx-edge secrets add ` are injected as environment variables into your functions, and declaring a `[telnyx]` binding injects `TELNYX_API_KEY`. Code that reads plain env vars therefore works locally by exporting the same names: ```bash # Locally: your shell export MY_API_KEY=test-value # Deployed: injected as an environment variable telnyx-edge secrets add MY_API_KEY ``` Use throwaway values locally and never commit real ones. See [Secrets](/docs/edge-compute/configuration/secrets) and [Environment variables](/docs/edge-compute/configuration/environment-variables). ## Deploy When it works locally, ship it: ```bash telnyx-edge ship # → 📡 Your function is live at: # https://-.telnyxcompute.com ``` Each successful ship creates an immutable revision; `telnyx-edge revisions list ` shows them and `telnyx-edge rollback ` retargets traffic to an earlier one. See [Deploy](/docs/edge-compute/deploy). ## Next Steps - [Deploy](/docs/edge-compute/deploy) — ship, revisions, and rollback - [HTTP handler](/docs/edge-compute/runtime/http-handler) — the exact entrypoint contract per language - [Bindings](/docs/edge-compute/runtime/bindings) — the `env` surface a deployed function gets - [CLI reference](/docs/edge-compute/reference/cli) — every `telnyx-edge` command --- ## Configuration ### Configuration > Source: https://developers.telnyx.com/docs/edge-compute/configuration.md Every Edge Compute project has a TOML manifest at its root. It is the one place a function is configured: it identifies what `telnyx-edge ship` deploys and declares the bindings the runtime resolves onto `env`. Everything below is a block in that file. There are two forms: - **`func.toml`** (classic) — a single function. Written by `telnyx-edge new-func`, which also registers the function server-side, so the UUID `func_id` is already filled in. Declares `[env_vars]`, `[telnyx]`, `[[secrets]]`, `[storage.kv.]`, `[storage.cloudstorage.]`, `[storage.sqldb.]`, and `[[ratelimits]]`. - **`telnyx.toml`** (umbrella) — a JavaScript or TypeScript project with a top-level `main` entry, bundled client-side on `ship`. Declares the same binding blocks plus `[[actors]]`, which classic projects cannot. `telnyx-edge types` reads either form and writes `telnyx-env.d.ts`, typing `env.` for each declaration. Configuration changes take effect on the next `telnyx-edge ship` — there is no live update; re-run `telnyx-edge types` after changing a supported binding declaration so `telnyx-env.d.ts` matches the manifest. The binding blocks — `[telnyx]`, `[[secrets]]`, `[storage.kv.]`, `[storage.cloudstorage.]`, `[storage.sqldb.]`, `[[actors]]` (umbrella only), and `[[ratelimits]]` — each resolve to a handle on `env`. This page documents their **manifest keys**; the [bindings catalogue](/docs/edge-compute/runtime/bindings#catalogue) lists every binding at a glance — declaration and `env` surface — and each block below links to its full documentation. ## func.toml `new-func` writes the minimal manifest — and because it registers the function server-side at scaffold time, the UUID `func_id` is already in it: ```toml [edge_compute] func_id = "7819cf01-39a8-400e-9bce-3d792ffa4017" func_name = "demo-ts" # For environment variables and secrets: # Use telnyx-edge secrets add # Secrets are injected as environment variables into all your functions ``` A manifest using every available block: ```toml [edge_compute] func_id = "7819cf01-39a8-400e-9bce-3d792ffa4017" # written by new-func — the function ship deploys func_name = "demo-ts" # → https://demo-ts-7819cf01-3.telnyxcompute.com (name + func_id prefix) [env_vars] # plain string env vars, injected on each deploy LOG_LEVEL = "info" MAX_RETRIES = "3" [telnyx] # pre-authenticated Telnyx API client binding = "MY_TELNYX" # → env.MY_TELNYX (TS); also injects TELNYX_API_KEY [[secrets]] # typed handle onto a stored secret binding = "STRIPE_KEY" # → env.SECRETS.get("STRIPE_KEY") name = "STRIPE_API_KEY" # the key stored with `secrets add` [storage.kv.MY_KV] # KV namespace binding — block key is the handle id = "550e8400-e29b-41d4-a716-446655440000" # → env.MY_KV [storage.cloudstorage.ASSETS] # Cloud Storage bucket binding — block key is the handle bucket_name = "my-assets" # → env.ASSETS; an existing bucket region = "us-east-1" # us-central-1 | us-east-1 | us-west-1 | eu-central-1 | ap-southeast-1 | ca-central-1 [storage.sqldb.DB] # SQL database binding — block key is the handle id = "550e8400-e29b-41d4-a716-446655440000" # → env.DB; the database UUID ``` There is no `language` key (the runtime comes from the project files the scaffold creates), no build block, and no timeout key — the request timeout is a platform property (default 30 s, maximum 60 s; see [Limits](/docs/edge-compute/platform/limits)). ### [edge_compute] — identity | Key | Value | |-----|-------| | `func_id` | The function's UUID, written by `new-func` when it registers the function server-side. This — not the directory — is what `ship` deploys, so swapping `func.toml` files switches deploy targets (the [CI/CD staging pattern](/docs/edge-compute/deploy#staging-and-production) relies on this). | | `func_name` | The function name, and the first part of the invoke URL: `https://{func_name}-{func_id-prefix}.telnyxcompute.com` (the suffix is derived from `func_id`; `telnyx-edge ship` and `list` print the exact host). See [Routes & Domains](/docs/edge-compute/configuration/routing). | ### [env_vars] — environment variables Free-form key-value pairs injected as process environment variables on each deploy. All values are strings; changes take effect on the next `ship`; values are plaintext in git — put credentials in secrets instead. **Dive in: [Environment Variables](/docs/edge-compute/configuration/environment-variables).** ### [[secrets]] — secret bindings | Key | Value | |-----|-------| | `binding` | The handle your code passes to `env.SECRETS.get()`. | | `name` | The stored secret key, from `telnyx-edge secrets add `. | The binding is the typed TypeScript surface; independently of it, every secret is injected as an environment variable into all functions in your organization. **Dive in: [Secrets](/docs/edge-compute/configuration/secrets).** ### [telnyx] — Telnyx API binding | Key | Value | |-----|-------| | `binding` | The property on `env` — `env.` is a pre-authenticated Telnyx SDK client in TypeScript functions. | Declaring the block also injects a `TELNYX_API_KEY` environment variable into the container — this is how non-TypeScript runtimes call the Telnyx API over plain REST. **Documented in [Telnyx API binding](/docs/edge-compute/telnyx-api).** ### [storage.kv.\] — KV namespace binding | Key | Value | |-----|-------| | block key `` | The handle — a name you choose. `env.` is a `KvNamespace` (get/put/delete/list). | | `id` | The KV namespace UUID, from `telnyx-edge storage kv create`. | Multiple blocks are allowed — each becomes its own `env` property. `telnyx-edge types` generates `KvNamespace` types for these blocks since CLI v0.2.4. **Documented in the [KV quick start](/docs/edge-compute/kv/quick-start).** ### [storage.cloudstorage.\] — Cloud Storage bucket binding | Key | Value | |-----|-------| | block key `` | The handle — a name you choose. `env.` is a `CloudStorageBucket` (get/put/head/delete/list). TypeScript-only, via `@telnyx/edge-runtime` ≥ 0.3.0. | | `bucket_name` | The name of an existing Cloud Storage bucket — the binding points at a bucket, it doesn't create one. | | `region` | The bucket's region: `us-central-1`, `us-east-1`, `us-west-1`, `eu-central-1`, `ap-southeast-1`, or `ca-central-1`. | Multiple blocks are allowed — each becomes its own `env` property. The runtime injects the credential, so no access key or secret key appears in your code. **Documented in [Cloud Storage binding](/docs/cloud-storage/bindings).** ### [storage.sqldb.\] — SQL database binding | Key | Value | |-----|-------| | block key `` | The handle — a name you choose. `env.` is a `SqlDatabase` (prepare/batch/exec). TypeScript-only, via `@telnyx/edge-runtime` ≥ 0.9.0. | | `id` | The database UUID, from `telnyx-edge storage sqldb create`. Binding by name, or creating a database from the manifest, is rejected before deploy. | Multiple blocks are allowed — each becomes its own `env` property, and two blocks carrying different ids are two separate databases. The id is checked when the function ships: an id that does not exist, belongs to another organization, or has not finished provisioning fails the deploy (currently as a generic `HTTP 500` that does not name the binding). **Documented in [SQL Databases](/docs/edge-compute/sqldb).** ## telnyx.toml The umbrella manifest replaces `[edge_compute]` with top-level keys and adds `[[actors]]`. On `ship`, the module graph rooted at `main` is bundled into a single file with esbuild (TypeScript/JavaScript only) and the manifest ships with it. ```toml name = "account-svc" # function name main = "src/index.ts" # entry module — exports the fetch handler (and the actor class) compatibility_date = "2026-05-01" [[actors]] binding = "ACCOUNT" # the property on env — your handle type = "Account" # the class to instantiate per name [[ratelimits]] name = "API_LIMIT" # → env.API_LIMIT.limit({ key }) limit = 100 # successful checks per key and window period = 60 # fixed window in seconds: 10 or 60 [telnyx] # same binding blocks as func.toml binding = "MY_TELNYX" [[secrets]] binding = "GREETING" name = "DEMO_GREETING" ``` | Key | Value | |-----|-------| | `name` | Function name, used by the platform to register the function. | | `main` | Entry module — the root of the client-side esbuild bundle. | | `compatibility_date` | Runtime compatibility pin. | | `[[actors]]` | Actor bindings — `binding` is the `env` property, `type` the class to instantiate per name. Ship-time constraints (identifier rules, uniqueness, the 32-character type cap) are specified in the [Stateful Actors configuration reference](/docs/edge-compute/stateful-actors/api-reference/configuration). | | `[[ratelimits]]` | Rate limiter bindings — `name` becomes the `env` property, `limit` is the per-key budget, and `period` is the fixed window in seconds (`10` or `60`). | The project shape — one module exporting both the actor class and a `fetch` handler — is covered in [Project Structure](/docs/edge-compute/stateful-actors/guides/project-structure). `telnyx-edge new-func --actor` scaffolds it. **Documented in [Stateful Actors](/docs/edge-compute/stateful-actors)** — the [configuration reference](/docs/edge-compute/stateful-actors/api-reference/configuration) covers the `[[actors]]` block in full. ### [[ratelimits]] — per-key rate limiting | Key | Value | |-----|-------| | `name` | Binding handle. It is uppercased and hyphens become underscores (`api-limit` → `env.API_LIMIT`). The resulting handle must be identifier-safe and unique across every binding in the manifest. | | `limit` | Positive integer: the maximum successful checks for one key in a window. | | `period` | Integer window duration in seconds. Only `10` and `60` are supported; duration strings such as `"60s"` are invalid. | | `namespace_id` | Optional. Functions sharing a `namespace_id` and a `name` share one set of counters; without it each function counts on its own. A positive integer as a string (`"1001"`) — not a KV namespace ID. | Multiple blocks are allowed and operate independently. The platform provisions their internal counter storage; there is no rate limiter resource to create first. **Documented in [Rate limiting](/docs/edge-compute/rate-limiting).** ## Configured outside the manifest The manifest references resources that exist outside it by name or ID — you create each one separately, and the block only points at it: - **Secret values** — stored server-side with `telnyx-edge secrets`; `[[secrets]]` and the injected environment variables only reference the key. See [Secrets](/docs/edge-compute/configuration/secrets). - **KV namespaces** — created with `telnyx-edge storage kv create`; `[storage.kv.]` only references the namespace `id`. See the [KV quick start](/docs/edge-compute/kv/quick-start). - **Cloud Storage buckets** — created in the [Mission Control portal](https://portal.telnyx.com/#/storage/buckets) or over the [S3-compatible API](/docs/cloud-storage/quick-start); `[storage.cloudstorage.]` only references an existing bucket by `bucket_name`. See [Cloud Storage binding](/docs/cloud-storage/bindings). - **SQL databases** — created with `telnyx-edge storage sqldb create`; `[storage.sqldb.]` only references the database `id`. See the [SQL Databases quick start](/docs/edge-compute/sqldb/quick-start). Rate limiters are the exception: a `[[ratelimits]]` block creates a platform-managed binding during deployment, so no separate resource or customer KV namespace is required. ## Ship-time validation Binding handles and `[env_vars]` names share one `env` namespace. `ship` (and `types`) enforce two hard rules and warn on a third: - **Duplicate `[[secrets]]` handles are rejected** — `ship` fails, because `env.SECRETS.get("")` would be ambiguous. - **A binding (or actor) named `SECRETS` is rejected** when a `[[secrets]]` block is declared — it conflicts with the `env.SECRETS` namespace. - **A name collision between `[env_vars]` and a binding — including an `[env_vars]` entry named `SECRETS` — only warns.** Both land on `env`, so one shadows the other and `ship` proceeds; rename one. - **Each `[[ratelimits]]` block in `telnyx.toml` is validated before upload.** `name` must be non-empty, exact names must not repeat, `limit` must be positive, and `period` must be `10` or `60`. The deployment service also rejects names that collide after normalization or with another binding section, and applies the same rules to a block declared in `func.toml`. ## Related - [Bindings catalogue](/docs/edge-compute/runtime/bindings#catalogue) — every binding at a glance: declaration and `env` surface for Telnyx API, Secrets, KV, Object storage, SQL databases, and Actors - [Environment variables](/docs/edge-compute/configuration/environment-variables) — everything that lands in the container's environment - [Secrets](/docs/edge-compute/configuration/secrets) — both access surfaces for `[[secrets]]` - [Telnyx API binding](/docs/edge-compute/telnyx-api) — the `[telnyx]` block and the `env.` client - [KV quick start](/docs/edge-compute/kv/quick-start) — the `[storage.kv.]` block and `KvNamespace` - [Cloud Storage binding](/docs/cloud-storage/bindings) — the `[storage.cloudstorage.]` block and `CloudStorageBucket` - [SQL Databases](/docs/edge-compute/sqldb) — the `[storage.sqldb.]` block and `SqlDatabase` - [Stateful Actors configuration](/docs/edge-compute/stateful-actors/api-reference/configuration) — the `[[actors]]` block in full - [Rate limiting](/docs/edge-compute/rate-limiting) — the `[[ratelimits]]` block and `env..limit({ key })` - [CLI reference](/docs/edge-compute/reference/cli) — `new-func` writes the manifest; `ship` and `types` read it --- ### Environment Variables > Source: https://developers.telnyx.com/docs/edge-compute/configuration/environment-variables.md Functions run as real containers, so configuration reaches your code as ordinary process environment variables — `process.env`, `os.environ`, `os.Getenv`, `System.getenv`. There is no separate configuration API to learn. ## What's in the environment | Variable | Where it comes from | |----------|---------------------| | `PORT` | Set by the platform. Your HTTP server must listen on it. | | Every `[env_vars]` key | Declared in `func.toml`; injected verbatim on each deploy. | | Every secret key | `telnyx-edge secrets add ` injects the key into **all** functions in your organization. See [Secrets](/docs/edge-compute/configuration/secrets). | | `TELNYX_API_KEY` | Injected when the function declares a `[telnyx]` binding. This is how non-TypeScript runtimes call the [Telnyx API](/docs/edge-compute/telnyx-api). | ## Declaring variables Define non-sensitive configuration under `[env_vars]` in `func.toml`: ```toml [edge_compute] func_id = "7819cf01-39a8-400e-9bce-3d792ffa4017" func_name = "demo-ts" [env_vars] LOG_LEVEL = "info" MAX_RETRIES = "3" DEBUG = "false" ``` Three behavioral contracts: - **All values are strings.** Parse numbers and booleans in your code. - **Changes take effect on the next `telnyx-edge ship`** — there is no live update. - **Names share the `env` namespace with bindings.** If an `[env_vars]` entry has the same name as a declared binding — or is named `SECRETS` while a `[[secrets]]` block is declared — `ship` **warns** that one shadows the other on `env` and still proceeds; rename one. (A *binding* named `SECRETS`, or a duplicate `[[secrets]]` handle, is a hard error.) `[env_vars]` values are plaintext in `func.toml` and end up in version control. Put credentials in [secrets](/docs/edge-compute/configuration/secrets) instead. ## Reading variables ```ts const port = Number(process.env.PORT ?? 8080); // set by the platform const logLevel = process.env.LOG_LEVEL ?? "info"; // from [env_vars] const maxRetries = Number(process.env.MAX_RETRIES ?? "3"); const debug = process.env.DEBUG === "true"; ``` ```python import os log_level = os.environ.get("LOG_LEVEL", "info") max_retries = int(os.environ.get("MAX_RETRIES", "3")) debug = os.environ.get("DEBUG", "false").lower() == "true" ``` ```go package function import ( "os" "strconv" ) func loadConfig() (logLevel string, maxRetries int, debug bool) { logLevel = os.Getenv("LOG_LEVEL") if logLevel == "" { logLevel = "info" } maxRetries, _ = strconv.Atoi(os.Getenv("MAX_RETRIES")) debug, _ = strconv.ParseBool(os.Getenv("DEBUG")) return } ``` ```java String logLevel = System.getenv().getOrDefault("LOG_LEVEL", "info"); int maxRetries = Integer.parseInt(System.getenv().getOrDefault("MAX_RETRIES", "3")); boolean debug = Boolean.parseBoolean(System.getenv("DEBUG")); ``` ## Environment variables vs secrets | | `[env_vars]` | Secrets | |--|--------------|---------| | Stored | Plaintext in `func.toml`, committed to git | Server-side; the CLI never prints values | | Scope | One function | Every function in the organization | | Changed by | Editing `func.toml`, then `ship` | `telnyx-edge secrets add`, then `ship` | | Use for | Log levels, feature flags, public URLs, tuning knobs | API keys, passwords, signing keys | ## Next Steps - [Secrets](/docs/edge-compute/configuration/secrets) — the server-side counterpart for sensitive values, including the typed `env.SECRETS` surface for TypeScript - [Bindings](/docs/edge-compute/runtime/bindings) — typed, pre-authenticated handles instead of raw variables - [Configuration](/docs/edge-compute/configuration) — the full manifest reference: every `func.toml` and `telnyx.toml` key --- ### Secrets > Source: https://developers.telnyx.com/docs/edge-compute/configuration/secrets.md Secrets are key-value pairs for sensitive data — API keys, database passwords, signing keys. They are scoped to your organization, stored server-side, and never displayed by the CLI after you set them. Every function receives them; there are two ways to read one. ## Managing secrets The `secrets` commands take positional arguments: ```bash # Add a secret — or update it, same command telnyx-edge secrets add STRIPE_API_KEY "sk_live_abc123" # ✓ Secret 'STRIPE_API_KEY' added successfully # List secret keys (values are never shown) telnyx-edge secrets list # SECRET ID SECRET NAME CREATED AT UPDATED AT # ------------ ---------------- ----------------- ----------------- # 1f4cafea-21ce-4e2b-9740-17d971c3d892 DATABASE_PASSWORD Jun 12, 2026, 09:41 Jun 12, 2026, 09:41 # 21ef7449-edbb-4248-b869-3d1352563a64 STRIPE_API_KEY May 28, 2026, 16:20 May 28, 2026, 16:20 # a209b1b3-062c-46f6-a2f2-3b0061751190 JWT_SECRET May 28, 2026, 16:19 May 28, 2026, 16:19 # Delete a secret telnyx-edge secrets delete OLD_API_KEY # ✓ Secret 'OLD_API_KEY' deleted successfully ``` Secrets are injected into function containers at deploy time — after adding or updating one, `telnyx-edge ship` each function that uses it. ## Reading secrets ### As environment variables — every language Each secret is injected into **all** functions in your organization as an environment variable named after its key. No declaration needed: ```ts const stripeKey = process.env.STRIPE_API_KEY; if (!stripeKey) throw new Error("STRIPE_API_KEY not configured"); ``` ```python import os stripe_key = os.environ["STRIPE_API_KEY"] ``` ```go stripeKey := os.Getenv("STRIPE_API_KEY") if stripeKey == "" { log.Fatal("STRIPE_API_KEY not configured") } ``` ```java String stripeKey = System.getenv("STRIPE_API_KEY"); ``` ### Through the typed binding — TypeScript TypeScript projects can additionally declare a `[[secrets]]` binding in `func.toml` and read the secret through `env.SECRETS`: ```toml # func.toml [[secrets]] binding = "STRIPE_KEY" # the handle your code uses name = "STRIPE_API_KEY" # the key stored with `secrets add` ``` ```bash telnyx-edge types # regenerates telnyx-env.d.ts from the manifest ``` ```ts import { env } from "@telnyx/edge-runtime"; const stripeKey = await env.SECRETS.get("STRIPE_KEY"); // get returns Promise ``` Both surfaces read the same store. The binding adds two things: `env.SECRETS.get` accepts only the literal union of declared handles — a typo'd handle fails to compile — and the in-code handle is decoupled from the stored key name, so you can swap `name` in the manifest without touching code. The binding SDK is TypeScript-only today; other runtimes use the injected environment variables. Enforced when `[[secrets]]` is declared: a **binding** named `SECRETS` and **duplicate `[[secrets]]` handles** are hard errors — `ship` fails. An `[env_vars]` entry named `SECRETS` only **warns** (it shadows the `env.SECRETS` namespace), so rename it. See [Bindings](/docs/edge-compute/runtime/bindings) for how the `env` namespace works. ## Rotating a secret `add` with an existing key overwrites its value: ```bash telnyx-edge secrets add DATABASE_PASSWORD "new-password" telnyx-edge ship # redeploy each function that uses it curl https://my-func-0198c2c5-8.telnyxcompute.com/health # verify ``` ## Scoping and local development Secrets are organization-scoped. There is no per-environment scoping (dev/staging/prod) today — if you need separation, encode it in the key name (`DEV_DATABASE_PASSWORD`, `PROD_DATABASE_PASSWORD`) and pick one in code. There is no local secrets emulation in the CLI. When running a function locally, export the same names as ordinary environment variables: ```bash STRIPE_API_KEY="sk_test_..." node index.js ``` ## Troubleshooting | Symptom | Fix | |---------|-----| | Variable missing in the function | `telnyx-edge secrets list` to confirm the key exists, then `telnyx-edge ship` — values are injected at deploy time. | | Stale value | `secrets add` again, then `ship`. | | `env.SECRETS.get` doesn't type-check | Re-run `telnyx-edge types` after editing `[[secrets]]`; pass the `binding` handle, not the `name`. | | CLI rejects the command | `telnyx-edge auth status`, then `telnyx-edge auth login` if needed. | ## Next Steps - [Environment variables](/docs/edge-compute/configuration/environment-variables) — the full picture of what lands in your container's environment - [Bindings](/docs/edge-compute/runtime/bindings) — the declare → `types` → `env` pattern all bindings share - [Configuration](/docs/edge-compute/configuration) — `[[secrets]]` and every other manifest key --- ## CI/CD ### CI/CD > Source: https://developers.telnyx.com/docs/edge-compute/deploy.md Every Edge Compute deployment from CI is the same three steps: install a pinned `telnyx-edge` binary, authenticate with `auth api-key set`, and run `ship`. This page gives you those steps as working pipelines for GitHub Actions, GitLab CI, and CircleCI, plus the patterns for staging/production and rollback. ## The three steps every pipeline runs ```bash TELNYX_EDGE_VERSION=v0.2.3 # 1. Install — release assets are version-stamped and extract into a versioned directory curl -fsSL "https://github.com/team-telnyx/edge-compute/releases/download/${TELNYX_EDGE_VERSION}/telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64.tar.gz" | tar xz sudo mv "telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64/telnyx-edge" /usr/local/bin/ # 2. Authenticate — the CLI does NOT read $TELNYX_API_KEY from the environment telnyx-edge auth api-key set "$TELNYX_API_KEY" # 3. Deploy the function in the current directory (or --from-dir ) telnyx-edge ship ``` Three facts these steps depend on: - **The CLI ships as GitHub release binaries only** — it is not on npm and there is no package manager formula. There is also no un-versioned "latest" asset: `releases/latest/download/...` URLs return 404. Pin a version in a `TELNYX_EDGE_VERSION` variable so bumping is a one-line change. For arm64 runners, use the `linux-arm64` asset. - **`telnyx-edge` does not read a `TELNYX_API_KEY` environment variable on its own.** Store your API key as a CI secret and run `telnyx-edge auth api-key set "$TELNYX_API_KEY"` as a pipeline step — it persists the key to `~/.telnyx-edge/config.toml` for the rest of the job. - **`ship` has no environment flag.** It deploys the function identified by `func.toml` in the shipped directory, and its flags are `--from-dir` and `--timeout` only. Staging and production are [separate functions](#staging-and-production). On success, `ship` prints the function's live URL (`https://{func-name}-{func-id-prefix}.telnyxcompute.com` — see [Routes & Domains](/docs/edge-compute/configuration/routing)). The URL is stable across deploys. ## GitHub Actions A complete workflow that tests on every push and deploys on pushes to `main`. Only the install, authenticate, and ship steps are Telnyx-specific — the test job is ordinary `npm` and assumes a committed lockfile and a `test` script (the scaffold ships neither); substitute your project's own checks. ```yaml # .github/workflows/deploy.yml name: Deploy Edge Function on: push: branches: [main] pull_request: branches: [main] env: TELNYX_EDGE_VERSION: v0.2.3 jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - run: npm ci - run: npm test deploy: needs: test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install telnyx-edge run: | curl -fsSL "https://github.com/team-telnyx/edge-compute/releases/download/${TELNYX_EDGE_VERSION}/telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64.tar.gz" | tar xz sudo mv "telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64/telnyx-edge" /usr/local/bin/ - name: Authenticate env: TELNYX_API_KEY: ${{ secrets.TELNYX_API_KEY }} run: telnyx-edge auth api-key set "$TELNYX_API_KEY" - name: Deploy run: telnyx-edge ship ``` Add the secret under **Settings → Secrets and variables → Actions → New repository secret**, named `TELNYX_API_KEY`. ## GitLab CI ```yaml # .gitlab-ci.yml stages: - deploy variables: TELNYX_EDGE_VERSION: v0.2.3 deploy: stage: deploy image: ubuntu:24.04 rules: - if: $CI_COMMIT_BRANCH == "main" before_script: - apt-get update -qq && apt-get install -y -qq curl ca-certificates - curl -fsSL "https://github.com/team-telnyx/edge-compute/releases/download/${TELNYX_EDGE_VERSION}/telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64.tar.gz" | tar xz - mv "telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64/telnyx-edge" /usr/local/bin/ - telnyx-edge auth api-key set "$TELNYX_API_KEY" script: - telnyx-edge ship ``` Define `TELNYX_API_KEY` as a **masked** variable under **Settings → CI/CD → Variables**. The `ubuntu:24.04` image runs as root, so no `sudo` is needed. ## CircleCI ```yaml # .circleci/config.yml version: 2.1 jobs: deploy: docker: - image: cimg/base:current environment: TELNYX_EDGE_VERSION: v0.2.3 steps: - checkout - run: name: Install telnyx-edge command: | curl -fsSL "https://github.com/team-telnyx/edge-compute/releases/download/${TELNYX_EDGE_VERSION}/telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64.tar.gz" | tar xz sudo mv "telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64/telnyx-edge" /usr/local/bin/ - run: name: Authenticate command: telnyx-edge auth api-key set "$TELNYX_API_KEY" - run: name: Deploy command: telnyx-edge ship workflows: deploy: jobs: - deploy: filters: branches: only: main ``` Set `TELNYX_API_KEY` as a project environment variable (**Project Settings → Environment Variables**) or in a context. ## Staging and production There is no `--env` flag and no environment promotion — `ship` always deploys the function that `func.toml` names. Environments are separate functions, e.g. `my-api-staging` and `my-api`, each with its own URL, secrets bindings, and revision history. Register both once, locally (`new-func` creates the function server-side and writes its UUID `func_id` into that directory's `func.toml` — this is a one-time setup step, not a CI step): ```bash telnyx-edge new-func -l=ts -n=my-api-staging telnyx-edge new-func -l=ts -n=my-api ``` Keep one codebase and both generated `func.toml` files in the repo; each pipeline job copies the matching one into place before shipping: ``` my-api/ ├── index.ts ├── package.json ├── func.toml # production — func_id of my-api └── deploy/ └── func.staging.toml # staging — func_id of my-api-staging ``` Because bindings are declared in `func.toml`, the two files can also point at per-environment resources — for example a separate [KV namespace](/docs/edge-compute/kv) id per environment. ```yaml # .github/workflows/deploy.yml — staging on main, production on v* tags name: Deploy on: push: branches: [main] tags: ['v*'] env: TELNYX_EDGE_VERSION: v0.2.3 jobs: deploy-staging: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install telnyx-edge run: | curl -fsSL "https://github.com/team-telnyx/edge-compute/releases/download/${TELNYX_EDGE_VERSION}/telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64.tar.gz" | tar xz sudo mv "telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64/telnyx-edge" /usr/local/bin/ - name: Authenticate env: TELNYX_API_KEY: ${{ secrets.TELNYX_API_KEY }} run: telnyx-edge auth api-key set "$TELNYX_API_KEY" - name: Ship the staging function run: | cp deploy/func.staging.toml func.toml telnyx-edge ship deploy-production: if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install telnyx-edge run: | curl -fsSL "https://github.com/team-telnyx/edge-compute/releases/download/${TELNYX_EDGE_VERSION}/telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64.tar.gz" | tar xz sudo mv "telnyx-edge-${TELNYX_EDGE_VERSION}-linux-amd64/telnyx-edge" /usr/local/bin/ - name: Authenticate env: TELNYX_API_KEY: ${{ secrets.TELNYX_API_KEY }} run: telnyx-edge auth api-key set "$TELNYX_API_KEY" - name: Ship the production function run: telnyx-edge ship ``` If you prefer fully separate directories over the `func.toml` swap, keep one function directory per environment and ship each with `telnyx-edge ship --from-dir `. If staging and production live in different Telnyx accounts, store one API key secret per account and reference the right one in each job. ## Rollback Every successful `ship` produces an immutable revision. Rolling back retargets traffic to a previous revision instantly — no rebuild, no re-upload: ```bash # List recent revisions (newest first) with their ids telnyx-edge revisions list my-api # Retarget traffic to a previous revision telnyx-edge rollback my-api a1b2c3d ``` Only revisions that reached `deploy_ok` can be rolled back to. You can wire these commands into a manually triggered pipeline job (e.g. `workflow_dispatch` on GitHub Actions), but they work just as well from a laptop — rollback does not need your source tree. A `git revert` + re-ship also works, but it goes through a full build; `rollback` is the fast path. ## Smoke test after deploy The function URL is stable, and the TypeScript/JavaScript scaffold answers `/health` with 200 — on other runtimes, point the check at a route your function serves. A post-deploy check is one step: ```yaml - name: Smoke test run: curl -fsS --retry 5 --retry-delay 2 --retry-all-errors https://my-api-.telnyxcompute.com/health ``` If it fails, roll back with `telnyx-edge rollback` as above. There is no platform metrics or logs surface to poll — see [Observability](/docs/edge-compute/observability) for what your function should emit instead. ## CI secrets vs. function secrets Two different things: | | Where it lives | What it's for | |---|---|---| | `TELNYX_API_KEY` | Your CI platform's secret store | Lets the pipeline run `auth api-key set` and `ship` | | Function secrets | The Telnyx platform, via `telnyx-edge secrets add ` | Values your function reads at runtime | Function secrets are not deployed from CI variables — manage them with the CLI (the arguments are positional). Values are injected into function containers at deploy time, so re-ship a function after changing a secret it uses. See [Secrets](/docs/edge-compute/configuration/secrets). ## Troubleshooting | Symptom | Fix | |---|---| | `ship` fails with an authentication error | The CLI does not pick up `$TELNYX_API_KEY` from the environment. Run `telnyx-edge auth api-key set "$TELNYX_API_KEY"` as a prior step; `telnyx-edge auth status` confirms. | | Install step 404s | The un-versioned `releases/latest/download/...` URL does not exist. Use the version-stamped asset URL shown above. | | Function stuck in `build_failed` or `deploy_failed` | `telnyx-edge reset-func ` tears down the failed deploy and returns the function to `created` (id, name, and config preserved), then re-ship. | | `ship` monitoring times out | Default monitoring timeout is 5 minutes. Raise it with `--timeout` (e.g. `telnyx-edge ship --timeout 10m`). | | Obscure failures | Re-run with `-v` for verbose logging. | ## Next Steps - [CLI reference](/docs/edge-compute/reference/cli) — every command and flag. - [Versions & Rollback](/docs/edge-compute/configuration/versions) — how revisions and rollback work. - [Secrets](/docs/edge-compute/configuration/secrets) — runtime secrets for your functions. - [Observability](/docs/edge-compute/observability) — what you can (and can't) see after a deploy. --- ### Routes & Domains > Source: https://developers.telnyx.com/docs/edge-compute/configuration/routing.md Every function deployed with `telnyx-edge ship` gets a public HTTPS URL. Requests to that URL are the only trigger — there are no cron, queue, or event triggers today. If you need scheduled invocation, point an external scheduler (for example, a GitHub Actions cron job) at the URL. ## Public URL pattern ``` https://{func-name}-{func-id-prefix}.telnyxcompute.com ``` | Component | Description | |-----------|-------------| | `func-name` | The `func_name` from your `func.toml` | | `func-id-prefix` | The first 10 characters of the function's `func_id` | A function named `hello-world` with `func_id` `0198c2c5-8f1e-7a3d-9b21-6e4a0d5f1c88` is served at: ``` https://hello-world-0198c2c5-8.telnyxcompute.com ``` `telnyx-edge ship` prints the URL after a successful deploy: ```bash 📡 Your function is live at: https://hello-world-0198c2c5-8.telnyxcompute.com 💡 Test your function: curl https://hello-world-0198c2c5-8.telnyxcompute.com ``` `telnyx-edge list` shows the invoke URL for every function in your organization. ## Calling your function All HTTP methods and paths under the function's URL are routed to your server — path handling is up to your code (see [HTTP handler](/docs/edge-compute/runtime/http-handler)): ```bash # GET request curl https://hello-world-0198c2c5-8.telnyxcompute.com # POST request with JSON body curl -X POST \ -H "Content-Type: application/json" \ -d '{"name": "test"}' \ https://hello-world-0198c2c5-8.telnyxcompute.com/anything ``` Requests time out after 30 seconds by default (60 seconds maximum) — see [Limits](/docs/edge-compute/platform/limits). ## Custom domains There is no custom domain support today — functions are reachable only at their `telnyxcompute.com` URL. To serve a function from your own domain, put a proxy you operate (CDN or reverse proxy) in front of it. ## Region placement You can't pin a function to a region today; the platform chooses placement. ## Next Steps - [HTTP handler](/docs/edge-compute/runtime/http-handler) — the entrypoint contract behind the URL - [Versions & Rollback](/docs/edge-compute/configuration/versions) — the URL always points at the active revision - [Limits](/docs/edge-compute/platform/limits) — request timeout and payload constraints --- ### Versions & Rollback > Source: https://developers.telnyx.com/docs/edge-compute/configuration/versions.md Every successful `telnyx-edge ship` produces an immutable revision. `telnyx-edge revisions list` shows a function's deploy history; `telnyx-edge rollback` retargets traffic to a previous revision without rebuilding or re-uploading anything. ## Listing revisions ```bash telnyx-edge revisions list my-func ``` Prints the most recent revisions, newest first: the revision ID, when it was shipped, who shipped it, and its deploy status. The revision currently serving traffic is marked with `*`. Revision IDs are short identifiers like `a1b2c3d` — you pass one to `rollback`. ## Rolling back ```bash telnyx-edge rollback my-func a1b2c3d # → Rollback of 'my-func' to revision a1b2c3d accepted; traffic is switching across clusters. # Run 'telnyx-edge revisions list my-func' to confirm the active revision. ``` Traffic is instantly retargeted to the existing, immutable revision across all clusters — there is no rebuild and no re-upload. Two constraints: - **The target must have reached `deploy_ok`.** A revision whose build or deploy failed never served traffic and can't be rolled back to; `revisions list` shows each revision's deploy status. - **Rollback doesn't touch your source.** Your working tree and git history are unchanged. The next `ship` deploys whatever is on disk — as a new revision — regardless of which revision is currently active. ## Rolling forward To move forward again, either `ship` — every successful ship creates a new revision and moves traffic to it — or `rollback` to any other revision that reached `deploy_ok`. ## Recovering a failed function Rollback assumes the function has a healthy revision to return to. A function stuck in a terminal failure state (`build_failed`, `deploy_failed`, `delete_failed`) can instead be reset: ```bash telnyx-edge reset-func broken-func ``` This tears down the function's deployed resources and returns it to the `created` state — preserving its ID, name, and config — so you can fix the code and `ship` again. A healthy function (`build_ok`/`deploy_ok`) can't be reset; use `delete-func` if you want it gone. ## Next Steps - [CI/CD](/docs/edge-compute/deploy) — ship from a pipeline; rollback is your escape hatch - [Routes & Domains](/docs/edge-compute/configuration/routing) — the function URL always points at the active revision - [CLI reference](/docs/edge-compute/reference/cli) — `ship`, `revisions`, `rollback`, and `reset-func` in full --- ## Runtime APIs ### Overview > Source: https://developers.telnyx.com/docs/edge-compute/runtime.md Most serverless platforms hand you a sandbox: a restricted runtime, a fixed set of provided APIs, one supported way to return a response. Edge Compute doesn't. A function is a **real Linux container** running your language's own runtime — so the bulk of your "runtime API" is just the standard library and any dependency you install, exactly as it behaves on any Linux box. What the platform adds on top is small and explicit, and this section documents it end to end: - **An execution environment** — how containers start, stay warm, scale, and get a request budget. This is the architecture your code runs inside. - **An entrypoint contract** — which file the platform runs and how a request reaches your code, per language. - **Bindings** — declared connections to platform resources: the Telnyx API, secrets, KV, and object storage, with credentials injected for you so nothing sensitive lives in your code. Everything else — HTTP parsing, crypto, file I/O, database drivers — comes from your language, not from the platform. ## Real containers Because a function is a real container: - **Native runtimes** — Node.js, Go, Python, and Java (Quarkus) run as themselves. No fetch-only sandbox, no restricted language subset. - **Any dependency that installs** — npm packages, Go modules, PyPI packages, Maven artifacts. - **POSIX environment** — environment variables, plus file I/O in the working directory and `/tmp`. The root filesystem is read-only and writes are ephemeral — they don't survive the container being recycled, so persist real data in [KV](/docs/edge-compute/kv) or a [bucket](/docs/cloud-storage/bindings). - **Outbound network** — HTTP clients, TCP sockets, DNS resolution. The trade-off is container lifecycle: instances cold-start, stay warm between requests, and are recycled. [Execution model](/docs/edge-compute/runtime/execution-model) covers what that means for initialization and in-memory state. ## The entrypoint contract HTTP is the only trigger. What "handling a request" means differs by language: | Language | `new-func -l` | Entrypoint | Server owned by | |---|---|---|---| | TypeScript | `ts` | `index.ts` — your own `node:http` server on `process.env.PORT \|\| 8080` | You | | JavaScript | `js` | `index.js` — same contract as TypeScript | You | | Go | `go` | `handler.go` — exported `Handle(w, r)` in `package function` | Platform | | Python | `python` | `function/func.py` — ASGI `new()` factory | Platform | | Java | `quarkus` | Quarkus Funqy `@Funq` method | Quarkus | The exact per-language contract — files, signatures, health probes, bodies, the request budget — is specified in [HTTP handler](/docs/edge-compute/runtime/http-handler). ## Bindings A function reaches platform resources — the Telnyx API, secrets, KV, and object storage — through **bindings** you declare in the project manifest. The platform injects the credential, so no keys or tokens appear in your code. How you reach a binding depends on the language: - **TypeScript** gets a typed handle for each declared binding, resolved at runtime. - **Go, Python, and Java** reach the same resources through injected environment variables (the Telnyx API key, each secret) and REST. [Bindings](/docs/edge-compute/runtime/bindings) documents every binding type and how to declare it; [Environment variables](/docs/edge-compute/configuration/environment-variables) and [Secrets](/docs/edge-compute/configuration/secrets) cover configuration. ## In this section How your code runs: cold starts, warm reuse, scaling, the request budget, and where state lives. The per-language entrypoint contract — files, signatures, health probes. Reach platform resources — Telnyx API, secrets, KV, object storage — from any language. --- ### Execution Model > Source: https://developers.telnyx.com/docs/edge-compute/runtime/execution-model.md An Edge Compute function is a Linux container running an HTTP server — one you run yourself in TypeScript and JavaScript, one run for you in Go, Python, and Java. The platform starts containers when traffic arrives, reuses them while it continues, and reclaims them — down to zero — when it stops. Everything on this page follows from that. ## Request path 1. **Route** — a request to `https://-.telnyxcompute.com` reaches the platform ([routing](/docs/edge-compute/configuration/routing)). 2. **Place** — a warm container takes it, or a new one starts (a cold start). 3. **Execute** — the server process handles the request and writes the response. 4. **Keep warm** — the container stays up for subsequent requests until it is recycled. ## Container lifecycle ### Cold start A cold start is the first request's cost of a new container: the image starts, the language runtime boots, your module-level code runs, and then the request is served. Put expensive setup — HTTP clients, connection pools, parsed config — at module scope so it runs once per container instead of once per request: ```ts import * as http from "node:http"; // Module scope — runs once per container, at cold start const startedAt = Date.now(); const cache = new Map(); // per-container cache — not durable const server = http.createServer((req, res) => { // Handler scope — runs per request res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ containerAgeMs: Date.now() - startedAt })); }); server.listen(process.env.PORT || 8080); ``` Curl that function twice: a near-zero `containerAgeMs` means the request paid a cold start; a growing one means the container was reused. The same split exists in every runtime — package-level `var`s and `init()` in Go, module scope or the optional `start(cfg)` hook in Python, application-scoped state in Quarkus. The per-language entrypoint contracts are in [HTTP handler](/docs/edge-compute/runtime/http-handler). ### Warm reuse While traffic continues, requests land on existing containers and skip initialization. Module state persists between requests **on the same container** — treat it as a cache keyed by container, nothing more. Two requests may or may not share a container, and the platform gives you no way to control which. ### Recycling and scale to zero Containers are reclaimed without notice: after idling, when a new revision is shipped (`telnyx-edge ship` — see [Versions](/docs/edge-compute/configuration/versions)), or by platform scaling decisions. At zero traffic a function scales to zero containers; the next request pays a cold start. Treat container memory like a process that can be killed at any instant: only what you wrote to durable storage is real. See [Where state lives](#where-state-lives) for what "durable storage" means here. In Python, your function class may define an optional `stop()` hook, called on scale-down or update — use it for best-effort cleanup, never for durability. ## Scaling The platform scales the container count with concurrent load. There is no concurrency knob to configure — scaling is automatic. | Traffic pattern | Platform response | |-----------------|-------------------| | Spike | New containers start — expect cold starts | | Sustained | Containers stay warm | | Falling | Containers are gradually reclaimed | | Zero | Scale to zero after an idle period | ## Request timeout A function must respond within **30 seconds** by default, **60 seconds** maximum; a request that exceeds the budget is terminated with a `504`. There is no `func.toml` field for this — see [Limits](/docs/edge-compute/platform/limits) for the full table. Budget outbound calls below the deadline so you return a real error instead of being cut off: ```ts const upstream = await fetch("https://api.example.com/data", { signal: AbortSignal.timeout(25_000), // fail at 25 s, inside the 30 s budget }); ``` ## Triggers **HTTP is the only trigger** — there are no cron, queue, or event triggers. A Telnyx webhook (a messaging profile or Call Control application pointed at your function URL) is just an HTTP request, so your function handles it like any other — see [Receiving messages](/docs/edge-compute/telnyx-api/receiving-messages) and [Handling calls](/docs/edge-compute/telnyx-api/handling-calls). For periodic work, call the URL from an external scheduler (a GitHub Actions cron job is enough), or use a [Stateful Actor](/docs/edge-compute/stateful-actors) [alarm](/docs/edge-compute/stateful-actors/alarms) to fire a callback on the platform itself. ## Where state lives Module state dies with the container, so anything that must survive needs a home: | Data | Use | Why | |------|-----|-----| | Per-entity state, counters, coordination | [Stateful Actors](/docs/edge-compute/stateful-actors) (Beta) | One instance per name, serialized calls, durable writes — correct under concurrency | | Cache entries, config, feature flags, sessions | [KV](/docs/edge-compute/kv) | Globally distributed reads; opaque values up to 1 MiB with optional TTL | | Files, media, large objects | [Cloud Storage buckets](/docs/cloud-storage/quick-start) | S3-compatible object storage | Don't build counters or per-entity coordination on KV — concurrent read-modify-write races there, which is exactly the problem Stateful Actors exist to solve. ## Next Steps - [HTTP handler](/docs/edge-compute/runtime/http-handler) — the entrypoint contract per language - [Limits](/docs/edge-compute/platform/limits) — timeouts, memory, and payload caps - [Versions](/docs/edge-compute/configuration/versions) — revisions, `ship`, and `rollback` --- ### HTTP Handler > Source: https://developers.telnyx.com/docs/edge-compute/runtime/http-handler.md HTTP is the only way a function is invoked. Requests arrive at `https://-.telnyxcompute.com` (see [Routing](/docs/edge-compute/configuration/routing)) and are handed to your entrypoint — but what "your entrypoint" means differs by language. In TypeScript and JavaScript you own and run the HTTP server (the CLI scaffolds a working one); in Go, Python, and Java the server is run for you and your code is called per request. `telnyx-edge new-func -l ` generates a working entrypoint for each language. The code on this page is that scaffold — start from it rather than a blank file. ## The contract at a glance | Language | File | You implement | Server owned by | Health probes | |---|---|---|---|---| | TypeScript / JavaScript | `index.ts` / `index.js` (project root) | An HTTP server on `process.env.PORT \|\| 8080` | You | You — return `200` for `/health` and paths under it | | Go | `handler.go` | `func Handle(w http.ResponseWriter, r *http.Request)` in `package function` | Platform | Handled for you — no health route in your code | | Python | `function/func.py` | `new()` factory returning an object with `async def handle(self, scope, receive, send)` | Platform | Platform — probes never reach `handle()` | | Java | `src/main/java/functions/Function.java` | A method annotated `@Funq` (Quarkus Funqy) | Quarkus | SmallRye Health at `/health/*`, pre-configured | ## The scaffold, by language Each tab is one language's entrypoint contract and the scaffold `new-func` generates for it. You own the server. There is no framework-provided `handler(request)` entrypoint and no `Response` object to return — your function is a container running a plain `node:http` server (or any server framework you install). Two things are contractual: - **Listen on `process.env.PORT`**, falling back to `8080`. - **Answer `/health`** (and paths under it) with a `200`. The platform's liveness and readiness probes hit it — a function that doesn't answer isn't routed traffic and can be restarted. Keep the probe path fast: respond before any other work. `index.ts` lives at the project root, next to `func.toml` — not in `src/`. The scaffold, lightly condensed (comments trimmed): ```ts import * as http from 'node:http'; interface ResponseData { message: string; data?: any; } const server = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => { // Liveness/readiness probes — answer before any other work if (req.url === '/health' || req.url?.startsWith('/health/')) { res.writeHead(200); res.end(); return; } const responseData: ResponseData = { message: 'Hello from Telnyx Edge Compute!' }; // POST: read and echo the request body if (req.method === 'POST') { let body = ''; req.on('data', (chunk: Buffer) => { body += chunk.toString(); }); req.on('end', () => { if (body) { try { responseData.data = JSON.parse(body); } catch (error) { responseData.data = body; // not JSON — echo as text } } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(responseData)); }); } else { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(responseData)); } }); const port = process.env.PORT || 8080; server.listen(port, () => { console.log(`Server running on port ${port}`); }); ``` The JavaScript scaffold (`-l js`) is the same file minus type annotations, at `index.js` (same project-root location). Request bodies arrive as `data` events carrying `Buffer` chunks. The scaffold accumulates them as a string, which is fine for text and JSON — for binary bodies collect the buffers instead (`chunks.push(chunk)` then `Buffer.concat(chunks)`), because `toString()` corrupts non-UTF-8 bytes. The platform owns the server. You write an ordinary `net/http` handler — exported as `Handle`, in `package function`, with no `main()`. The platform binds the port and routes requests to `Handle`; the scaffold defines no health route and doesn't need one. `handler.go` — the scaffold, lightly condensed (comments trimmed): ```go package function import ( "encoding/json" "io" "log" "net/http" ) // Response represents the function response structure type Response struct { Message string `json:"message"` Data interface{} `json:"data,omitempty"` } // Handle answers every HTTP request routed to the function. func Handle(w http.ResponseWriter, r *http.Request) { log.Printf("Serving a new request: %v > %v @ %v", r.Host, r.Method, r.URL.Path) w.Header().Set("Content-Type", "application/json") response := Response{ Message: "Hello from Telnyx Edge Compute!", } // POST: read and echo the request body if r.Method == "POST" { body, err := io.ReadAll(r.Body) if err != nil { log.Printf("Error reading request body: %v", err) } else if len(body) > 0 { var jsonData interface{} if err := json.Unmarshal(body, &jsonData); err == nil { response.Data = jsonData } else { response.Data = string(body) // not JSON — echo as text } } } responseBytes, err := json.Marshal(response) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } w.Write(responseBytes) } ``` `go.mod` declares `module function` (Go 1.24). Standard `net/http` semantics apply: read the body from `r.Body`, set headers with `w.Header().Set(...)` before the first write. The contract is ASGI. Your project is a `function/` package whose `func.py` exposes a module-level `new()` factory; the runtime calls it when an instance starts, keeps the returned object, and dispatches every HTTP request — except liveness and readiness probes, which the platform answers itself — to its `handle` coroutine. There is no `handler(request)` returning a dict, and no `requirements.txt` — dependencies go in `pyproject.toml` (the scaffold uses hatchling). `function/func.py` — the scaffold, trimmed (scope validation and logging removed): ```python import json def new(): """Required. Called when an instance starts; the returned object receives all requests via its handle() coroutine.""" return Function() class Function: async def handle(self, scope, receive, send): """Called for every HTTP request except liveness/readiness probes.""" method = scope.get('method', 'GET') response_data = {"message": "Hello from Telnyx Edge Compute!"} # POST: the body arrives in chunks over the ASGI receive channel if method == 'POST': body = b'' while True: message = await receive() if message['type'] == 'http.request': body += message.get('body', b'') if not message.get('more_body', False): break if body: try: response_data["data"] = json.loads(body) except (json.JSONDecodeError, UnicodeDecodeError): response_data["data"] = body.decode('utf-8', errors='replace') await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'application/json'], ], }) await send({ 'type': 'http.response.body', 'body': json.dumps(response_data).encode(), }) def start(self, cfg): """Optional. Called when an instance starts (scale-up, update). cfg is a dict of all environmental configuration.""" def stop(self): """Optional. Called when an instance stops (scale-down, update, manual cancel).""" ``` `function/__init__.py` re-exports the factory: `from .func import new`. | Hook | Required | Called when | |---|---|---| | `new()` | Yes | Instance start — returns the object that handles requests | | `handle(self, scope, receive, send)` | Yes | Every request (async, ASGI) | | `start(cfg)` | No | Instance start — `cfg` is a dict of all environmental configuration | | `stop()` | No | Instance stop — scale-down, update, or manual cancel | Functions are Quarkus Funqy functions: a plain class with a method annotated `@Funq` that takes a bean and returns a bean. Quarkus owns the server, deserializes the JSON request body into your input bean, and serializes your return value back to JSON. `src/main/java/functions/Function.java` — the scaffold: ```java package functions; import io.quarkus.funqy.Funq; public class Function { @Funq public Output function(Input input) { String defaultMessage = "Hello from Telnyx Edge Compute!"; // Null or empty input — return the default message if (input == null || input.getMessage() == null || input.getMessage().trim().isEmpty()) { return new Output(defaultMessage); } // Echo back the input message return new Output(input.getMessage()); } } ``` `Input` and `Output` are plain beans in the same package — a `message` field with a no-arg constructor, getter, and setter. Invocation is JSON in, JSON out: ```bash curl -s https://demo-quarkus-.telnyxcompute.com \ -H "Content-Type: application/json" \ -d '{"message": "hello"}' # → {"message":"hello"} ``` Two scaffold defaults worth knowing: - `application.properties` selects the exported method by name: `quarkus.funqy.export=function`. Rename the method, update the property. - Health endpoints come from SmallRye Health, pre-configured at `/health/liveness` and `/health/readiness` — leave them in place. Funqy is a typed JSON model: your method sees the deserialized bean, not raw bytes, URL paths, or headers. For raw HTTP semantics — routing on paths, custom headers, binary bodies — use the TypeScript, JavaScript, Go, or Python contract instead. ## Bodies, headers, and binary data These apply to the raw-HTTP contracts — TypeScript, JavaScript, Go, and Python. Java/Funqy is the exception: it's typed JSON in/out, so raw bodies, binary responses, and custom headers aren't available from a `@Funq` method (see the Java tab). - **Bodies pass through raw, both directions.** There is no base64 envelope and no JSON wrapping between the caller and your code. To serve binary, set the `Content-Type` and write the bytes: ```ts res.writeHead(200, { "Content-Type": "image/png" }); res.end(pngBuffer); // raw bytes — no base64 ``` - **Headers are yours.** Whatever your server (or `Handle`, or `http.response.start`) sets is what the caller receives. There is no platform header rewriting to work around. - **Bodies are size-capped.** Request and response body limits are listed in [Limits](/docs/edge-compute/platform/limits). ## The request budget A function has **30 seconds** by default to respond, and **60 seconds** at most. Past the budget the request is terminated and the caller gets a `504`. This is a platform limit, not a `func.toml` field — there is no `timeout_seconds` setting. For work that can run long, set your own internal timeout a few seconds under the platform's and return an error or partial result instead of being cut off mid-response. Exact numbers and the other caps — memory, body size, deploy rate — are in [Limits](/docs/edge-compute/platform/limits). ## Related - [Bindings](/docs/edge-compute/runtime/bindings) — the typed `env` surface: Telnyx API, secrets, KV - [Execution model](/docs/edge-compute/runtime/execution-model) — lifecycle, cold starts, concurrency - [Limits](/docs/edge-compute/platform/limits) — timeouts, body size, memory --- ### Overview > Source: https://developers.telnyx.com/docs/edge-compute/runtime/bindings.md A binding maps a name you declare in the project manifest to an authenticated resource handle, resolved by the runtime — the credential is injected for you and never appears in your code, bundle, or logs. Each binding resolves on the `env` object (from `@telnyx/edge-runtime`) — `env.MY_TELNYX`, `env.SECRETS`, and so on. The `env` object and `telnyx-edge types` are **TypeScript-only** today. Other runtimes (`js`, `go`, `python`, `quarkus`) don't get the typed `env` handle, but reach the same resources through the credentials injected into the container — see [Bindings from other languages](#bindings-from-other-languages). ## Every binding works the same way ```toml # func.toml — 1. declare [telnyx] binding = "MY_TELNYX" ``` ```bash # 2. generate types telnyx-edge types ``` ```ts // 3. use it — typed and authenticated import { env } from "@telnyx/edge-runtime"; const { data } = await env.MY_TELNYX.availablePhoneNumbers.list({ filter: { country_code: "US" }, }); ``` The binding name (`MY_TELNYX`) is yours to choose; it becomes the property on `env`. `telnyx-edge types` writes `telnyx-env.d.ts` from the manifest — re-run it after every binding change. Typing for `[storage.kv.]` blocks requires CLI **v0.2.3** or later. The SDK types `.data` as `T | undefined` for list calls. Under `tsc --strict`, indexing into `data` (e.g. `data.length`) fails with `TS18048: 'data' is possibly 'undefined'`. Coalesce before use: `const arr = list.data ?? [];`. ## Catalogue | Resource | Declaration | On `env` | |----------|-------------|----------| | [Telnyx API](/docs/edge-compute/telnyx-api) | `[telnyx]` | `env.` — a pre-authenticated Telnyx SDK client | | [Secrets](/docs/edge-compute/configuration/secrets) | `[[secrets]]` | `env.SECRETS.get("")` → `Promise` | | [Rate limiting](/docs/edge-compute/rate-limiting) | `[[ratelimits]]` | `env..limit({ key })` → `Promise<{ success: boolean }>` | | [Key-Value storage](/docs/edge-compute/kv) | `[storage.kv.]` | `env.` — a [`KvNamespace`](/docs/edge-compute/kv/reference/kv-namespace): `get`, `put`, `delete`, `list` | | [Object storage](/docs/cloud-storage/bindings) | `[storage.cloudstorage.]` | `env.` — a [`CloudStorageBucket`](/docs/cloud-storage/bindings/reference): `get`, `put`, `head`, `delete`, `list` | | [SQL Databases](/docs/edge-compute/sqldb) (Beta) | `[storage.sqldb.]` — `id` must be the database UUID | `env.` — a [`SqlDatabase`](/docs/edge-compute/sqldb/reference/sql-database): `prepare`, `batch`, `exec` | | [Stateful Actors](/docs/edge-compute/stateful-actors) (Beta) | `[[actors]]` — umbrella `telnyx.toml` only | `env.` — an actor namespace: one instance per name, addressed via `idFromName` | ## Manifest: `func.toml` or `telnyx.toml` Bindings are declared in your project manifest. `telnyx-edge types` reads either form and types `env.` for each declared binding. - **`func.toml`** (classic) — the standard `[edge_compute]` project file. Can declare `[telnyx]`, `[[secrets]]`, `[storage.kv.]`, `[storage.cloudstorage.]`, `[storage.sqldb.]`, and `[[ratelimits]]`. - **`telnyx.toml`** (umbrella) — a manifest with top-level `name` and `main`. Declares the same bindings, plus `[[actors]]`, whose classes are imported from `main`. ```toml # telnyx.toml — umbrella manifest name = "my-app" main = "src/index.ts" [telnyx] binding = "MY_TELNYX" [[secrets]] binding = "GREETING" name = "DEMO_GREETING" [storage.kv.CACHE] id = "" # from `telnyx-edge storage kv list` [[ratelimits]] name = "API_LIMIT" # → env.API_LIMIT limit = 100 period = 60 ``` ## Bindings from other languages The `env` SDK surface is TypeScript-only, but the credentials behind it are not: - **Telnyx API** — declaring `[telnyx]` also injects a `TELNYX_API_KEY` environment variable into the container at runtime. Any language can call the Telnyx REST API with it as a bearer token — see [Using the Telnyx API](/docs/edge-compute/telnyx-api). - **Secrets** — every secret is also injected as a plain environment variable into all your functions (`os.environ["DEMO_GREETING"]`, `os.Getenv("DEMO_GREETING")`, …). `env.SECRETS.get()` and the environment variable are two views of the same value. - **KV** — any language can use the [KV REST API](/docs/edge-compute/kv/quick-start#path-b-the-rest-api) with the injected `TELNYX_API_KEY`. - **Object storage** — the typed `env` binding is TypeScript only; from any language, reach the same buckets over the [S3-compatible API](/docs/cloud-storage/quick-start) with your own access keys. - **SQL Databases** — the typed `env` binding is TypeScript only; from any language, query the same database over [`POST /v2/storage/sqldbs/{id}/actions/query`](/docs/edge-compute/sqldb/quick-start#8-query-it-from-any-language) with a Telnyx API key as a bearer token. `TELNYX_API_KEY` is only in the environment when the function also declares `[telnyx]`, so declare that block as well or supply the key as a [secret](/docs/edge-compute/configuration/secrets). That endpoint takes no bound parameters, so use it for SQL you wrote yourself, never for values that came from a caller. - **Stateful Actors** — TypeScript only; there is no REST fallback today. - **Rate limiting** — the typed `env` binding is TypeScript only, and there is no environment-variable equivalent. ## Bindings vs secrets - **Binding** — a Telnyx or platform resource, authenticated for you (`env.MY_TELNYX`). - **Secret** — a value you supply (`env.SECRETS.get("STRIPE_KEY")`). Use a binding for platform resources; use a [secret](/docs/edge-compute/configuration/secrets) for your own third-party credentials. ## Next Steps - [Telnyx API quick start](/docs/edge-compute/telnyx-api/quick-start) — declare `[telnyx]` and make your first authenticated call - [KV quick start](/docs/edge-compute/kv/quick-start) — create a namespace, bind it, read and write - [Object storage binding](/docs/cloud-storage/bindings) — bind a bucket and read, write, and list objects from `env` - [SQL Databases quick start](/docs/edge-compute/sqldb/quick-start) — create a database, bind it, and query it from `env.DB` - [Secrets](/docs/edge-compute/configuration/secrets) — add, rotate, and access secrets - [Stateful Actors](/docs/edge-compute/stateful-actors) — per-entity state and coordination - [Rate limiting](/docs/edge-compute/rate-limiting) — enforce per-key request budgets with a platform-managed counter --- ### Overview > Source: https://developers.telnyx.com/docs/edge-compute/telnyx-api.md The Telnyx API binding puts a ready-to-use, authenticated Telnyx client on `env`. You never handle an API key — the binding injects credentials at the edge and keeps them out of your code, bundle, and logs. ```ts import { env } from "@telnyx/edge-runtime"; await env.MY_TELNYX.messages.send({ from: "+13125550100", to: "+13125550101", text: "Hello from Edge Compute", }); ``` - **Free-formed name** — `MY_TELNYX` is whatever you set as `binding` in `func.toml`. It becomes the property on `env`. - **Typed** — `telnyx-edge types` types `env.MY_TELNYX` as the Telnyx client. - **One per organization** — every function in the org shares it. Start with the [Quick start](/docs/edge-compute/telnyx-api/quick-start). The org-level credential behind the binding (`bindings create` / `validate` / `update`) is account-level and rarely touched — see the [CLI reference](/docs/edge-compute/reference/cli#bindings). --- ### Quick Start > Source: https://developers.telnyx.com/docs/edge-compute/telnyx-api/quick-start.md A complete function that returns your Telnyx account balance — declared, typed, shipped, and called. ## 1. Create a function ```bash telnyx-edge new-func --language ts --name balance-check cd balance-check ``` ## 2. Declare the binding Add a `[telnyx]` block to the generated `func.toml`: ```toml # func.toml [edge_compute] func_id = "60c5ce48-…" # created by new-func func_name = "balance-check" [telnyx] binding = "MY_TELNYX" ``` ## 3. Generate types ```bash telnyx-edge types ``` `env.MY_TELNYX` is now typed as the Telnyx client. ## 4. Write the handler ```ts // index.ts import * as http from "node:http"; import { env } from "@telnyx/edge-runtime"; const port = Number(process.env.PORT ?? 8080); http.createServer(async (req, res) => { // Match /health and any /health/* probe path the platform requires // (the scaffold generates this guard — keep it). if (req.url === "/health" || req.url?.startsWith("/health/")) { return res.writeHead(200).end(); } const { data } = await env.MY_TELNYX.balance.retrieve(); res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(data)); }).listen(port); ``` ## 5. Ship ```bash telnyx-edge ship ``` ## 6. Call it `ship` prints your function URL. Hit it: ```bash curl https://balance-check-.telnyxcompute.com ``` ```json { "credit_limit": "1000.00", "frozen": "0.00", "currency": "USD", "available_credit": "1100.00", "pending": "0.00", "balance": "100.00", "record_type": "balance" } ``` --- ### API Reference > Source: https://developers.telnyx.com/docs/edge-compute/telnyx-api/api-reference.md `env.MY_TELNYX` is a ready-to-use, authenticated Telnyx client handle — call it like any Telnyx API client, with auth already wired in (no `new Telnyx(...)`, no API key to manage). Calls take the shape `env.MY_TELNYX..(...)`, using resource and method names — not raw HTTP paths: - `` — a camelCase property: `messages`, `calls`, `balance`, `availablePhoneNumbers`, … - `` — a method on the resource: `.send`, `.dial`, `.list`, `.retrieve`, … The names don't always track the HTTP API (`messages.send` is `POST /messages`, but `messages.cancelScheduled` is `DELETE /messages/{id}`), so discover them rather than guessing from endpoints: - **Autocomplete** — after [`telnyx-edge types`](/docs/edge-compute/telnyx-api/quick-start), your editor completes `env.MY_TELNYX.` with every resource and method. - **[Client method reference (`api.md`)](https://github.com/team-telnyx/telnyx-node/blob/master/api.md)** — lists every `.(...)` and the endpoint it maps to. - **[HTTP API reference](/api-reference)** — endpoint parameters and behavior. ```ts import { env } from "@telnyx/edge-runtime"; await env.MY_TELNYX.messages.send({ from: "+13125550100", to: "+13125550101", text: "Hello" }); // POST /messages await env.MY_TELNYX.calls.dial({ connection_id: "2985…", from: "+13125550100", to: "+13125550101" }); // POST /calls await env.MY_TELNYX.availablePhoneNumbers.list({ filter: { country_code: "US" } }); // GET /available_phone_numbers await env.MY_TELNYX.balance.retrieve(); // GET /balance ``` Each method returns the API response; list and retrieve calls expose the payload on `.data`. ## Errors Calls reject on API errors. Catch and inspect: ```ts try { await env.MY_TELNYX.messages.send({ from: "+13125550100", to: "+13125550101", text: "Hello" }); } catch (err) { // err.status (e.g. 400), err.message, err.error (parsed body) } ``` --- ### Receiving Messages > Source: https://developers.telnyx.com/docs/edge-compute/telnyx-api/receiving-messages.md Inbound SMS is webhook-driven: Telnyx POSTs a `message.received` event to your messaging profile's webhook, and your Edge Compute function is that webhook. Replying to another Telnyx number is **on-net** — no 10DLC campaign required. ## 1. Write the handler ```ts // index.ts import * as http from "node:http"; import { env } from "@telnyx/edge-runtime"; const port = Number(process.env.PORT ?? 8080); const received: any[] = []; // in-memory; use KV or SQL DB for durable storage function body(req: http.IncomingMessage): Promise { return new Promise((r) => { let b = ""; req.on("data", (c) => (b += c)); req.on("end", () => r(b)); }); } http.createServer(async (req, res) => { // GET: see what's been received if (req.method === "GET") { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ received }, null, 2)); return; } // POST: Telnyx inbound webhook — parse it through the binding const evt = env.MY_TELNYX.webhooks.unsafeUnwrap<{ data: any }>(await body(req)).data; if (evt?.event_type === "message.received") { const p = evt.payload; const from = p.from.phone_number; const to = p.to[0].phone_number; received.unshift({ from, to, text: p.text, at: evt.occurred_at }); // Auto-reply on-net (no 10DLC when the recipient is a Telnyx number) await env.MY_TELNYX.messages.send({ from: to, to: from, text: `You said: ${p.text}` }); } res.writeHead(200); res.end(); }).listen(port); ``` Declare the binding in `func.toml` (see the [Quick start](/docs/edge-compute/telnyx-api/quick-start)): ```toml [telnyx] binding = "MY_TELNYX" ``` ## 2. Ship ```bash telnyx-edge ship ``` ## 3. Point a messaging profile at it Set a messaging profile's inbound webhook to your function URL, then assign your number to that profile: ```bash # webhook -> your function curl -X POST https://api.telnyx.com/v2/messaging_profiles \ -H "Authorization: Bearer $TELNYX_API_KEY" -H "Content-Type: application/json" \ -d '{"name":"inbound-demo","webhook_url":"https://YOUR-FUNC.telnyxcompute.com","whitelisted_destinations":["US"]}' # assign your number to the profile (use the profile id from the response) curl -X PATCH https://api.telnyx.com/v2/phone_numbers/YOUR-NUMBER-ID/messaging \ -H "Authorization: Bearer $TELNYX_API_KEY" -H "Content-Type: application/json" \ -d '{"messaging_profile_id":"YOUR-PROFILE-ID"}' ``` ## 4. Test on-net Send from another Telnyx number on your account to your function's number: ```bash curl -X POST https://api.telnyx.com/v2/messages \ -H "Authorization: Bearer $TELNYX_API_KEY" -H "Content-Type: application/json" \ -d '{"from":"+1ANOTHER_TELNYX_NUMBER","to":"+1YOUR_FUNC_NUMBER","text":"hello"}' ``` You get back **"You said: hello"** on-net, and `GET https://YOUR-FUNC.telnyxcompute.com` shows what arrived. The inbound event the function parses looks like: ```json { "data": { "event_type": "message.received", "occurred_at": "2026-06-19T16:06:17.464+00:00", "payload": { "from": { "phone_number": "+1..." }, "to": [ { "phone_number": "+1..." } ], "text": "hello" } } } ``` **Only on-net replies skip 10DLC.** Receiving is always free. Replying to a Telnyx number is on-net (no campaign). Replying to an off-net number — e.g. a personal mobile — is application-to-person traffic and requires 10DLC registration. ## Keyword auto-reply The handler above echoes every message. To answer commands instead, replace the `messages.send` call in step 1 with a keyword match: ```ts const text = p.text.trim().toUpperCase(); if (text === "STOP") { // opt-out: send nothing } else { const replies: Record = { HELP: "Commands: HOURS, LOCATION. Reply STOP to opt out.", HOURS: "Open Mon-Fri 9am-5pm ET.", LOCATION: "600 Congress Ave, Austin, TX", }; await env.MY_TELNYX.messages.send({ from: to, to: from, text: replies[text] ?? "Thanks for your message. Reply HELP for options.", }); } ``` This handles the STOP message itself but doesn't remember it — store opted-out numbers in [KV](/docs/edge-compute/kv) and check before every send. --- ### Handling Calls > Source: https://developers.telnyx.com/docs/edge-compute/telnyx-api/handling-calls.md Inbound voice is webhook-driven through Call Control: Telnyx POSTs call events to your Call Control application's webhook, and your Edge Compute function is that webhook. On `call.initiated` you answer the call; on `call.answered` you play audio. ## 1. Write the handler ```ts // index.ts import * as http from "node:http"; import { env } from "@telnyx/edge-runtime"; const port = Number(process.env.PORT ?? 8080); const AUDIO_URL = "https://YOUR-HOST/song.mp3"; // a reachable HTTPS mp3/wav you have rights to function body(req: http.IncomingMessage): Promise { return new Promise((r) => { let b = ""; req.on("data", (c) => (b += c)); req.on("end", () => r(b)); }); } http.createServer(async (req, res) => { if (req.method === "GET") { res.writeHead(200).end(); return; } // health // parse the inbound webhook through the binding const evt = env.MY_TELNYX.webhooks.unsafeUnwrap<{ data: any }>(await body(req)).data; const id = evt?.payload?.call_control_id; if (evt?.event_type === "call.initiated" && id) { await env.MY_TELNYX.calls.actions.answer(id, {}); } else if (evt?.event_type === "call.answered" && id) { await env.MY_TELNYX.calls.actions.startPlayback(id, { audio_url: AUDIO_URL }); } res.writeHead(200); res.end(); }).listen(port); ``` Declare the binding in `func.toml`: ```toml [telnyx] binding = "MY_TELNYX" ``` ## 2. Ship ```bash telnyx-edge ship ``` ## 3. Point a Call Control app at it ```bash # create a Call Control app whose webhook is your function curl -X POST https://api.telnyx.com/v2/call_control_applications \ -H "Authorization: Bearer $TELNYX_API_KEY" -H "Content-Type: application/json" \ -d '{"application_name":"voice-demo","webhook_event_url":"https://YOUR-FUNC.telnyxcompute.com"}' # route your number to that app (connection_id = the app id from the response) curl -X PATCH https://api.telnyx.com/v2/phone_numbers/YOUR-NUMBER-ID \ -H "Authorization: Bearer $TELNYX_API_KEY" -H "Content-Type: application/json" \ -d '{"connection_id":"YOUR-CALL-CONTROL-APP-ID"}' ``` ## 4. Test Call the number from any phone. The function answers and plays your audio. `audio_url` must be a publicly reachable HTTPS `.mp3` or `.wav`. The flow is two events — `call.initiated` (answer) then `call.answered` (play) — so handle both. To loop, hang up, or chain more actions, respond to later events (`call.playback.ended`, `call.hangup`) the same way. ## Time-of-day routing To route callers to a person instead of playing audio, replace both event branches in step 1 with a single `transfer` on `call.initiated` — Telnyx dials the destination and bridges the caller when it answers, so there's nothing to do on `call.answered`: ```ts const OFFICE = "+13125550100"; // business hours const ON_CALL = "+13125550199"; // after hours function businessHours(): boolean { const hour = Number( new Intl.DateTimeFormat("en-US", { timeZone: "America/New_York", hour: "numeric", hourCycle: "h23", }).format(new Date()), ); return hour >= 9 && hour < 17; } if (evt?.event_type === "call.initiated" && id) { await env.MY_TELNYX.calls.actions.transfer(id, { to: businessHours() ? OFFICE : ON_CALL, }); } ``` If the transfer fails, you get a `call.hangup` webhook for the destination leg and the caller's leg stays active — transfer to an alternate number or answer and play a message. --- ### Overview > Source: https://developers.telnyx.com/docs/edge-compute/rate-limiting.md A rate limiter binding caps how many requests a key can make during a fixed window. Your function chooses the key—for example, an authenticated user ID, tenant ID, or hashed API-key identifier—and decides how to respond when the budget is exhausted. Rate limiters are available to JavaScript and TypeScript functions in either project shape. In a `telnyx.toml` project the binding arrives on the handler's `env` argument as `env.`; in a `func.toml` project reach it with `import { env } from "@telnyx/edge-runtime"`. You don't create a KV namespace or manage counter storage. Start with the [Quick Start](/docs/edge-compute/rate-limiting/quick-start) to add a rate limiter to a function and test it in production. ## Configuration reference | Field | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Binding name. The platform uppercases it and replaces hyphens with underscores, so `api-limit` is exposed as `env.API_LIMIT`. The resulting handle must start with a letter or underscore, contain only letters, numbers, and underscores, and be unique across all bindings in the manifest. | | `limit` | integer | Yes | Maximum number of successful checks per key and window. Must be greater than zero. | | `period` | integer | Yes | Fixed-window duration in seconds. The supported values are `10` and `60`. | | `namespace_id` | string | No | Groups counters across functions: functions declaring the same `namespace_id` **and** the same `name` count against one budget, and omitting it gives each function its own. A positive integer written as a string, such as `"1001"` — it is **not** a KV namespace ID, and rate limiter counters never occupy one of your KV namespaces. Set `limit` and `period` identically in every function sharing a group; they are read per function, so a mismatch means one starts rejecting at another's threshold. | The period is an integer, not a duration string: use `period = 60`, not `period = "60s"`. Configuration changes take effect after the next `telnyx-edge ship`. ## Multiple rate limiters Each `[[ratelimits]]` block has an independent counter namespace. This lets one function apply different budgets to different plans or operations: ```toml [[ratelimits]] name = "FREE_TIER" limit = 100 period = 60 [[ratelimits]] name = "PAID_TIER" limit = 1000 period = 60 ``` ```ts type Tier = "free" | "paid"; async function checkPlanLimit( env: { FREE_TIER: RateLimiter; PAID_TIER: RateLimiter }, userId: string, tier: Tier, ): Promise { const limiter = tier === "paid" ? env.PAID_TIER : env.FREE_TIER; const result = await limiter.limit({ key: userId }); return result.success; } ``` Derive both `userId` and `tier` from authenticated, trusted application state. A caller-controlled header or query parameter lets a client choose a fresh key and bypass its intended budget. ## Choose keys carefully | Key | When to use it | |---|---| | Authenticated user ID | Give every signed-in user an independent budget. | | Tenant ID | Share one budget across all users in a tenant. | | Hashed API-key ID | Limit an API credential without placing the raw secret in a counter key. | | Composite key such as `tenant:endpoint` | Apply separate budgets to operations or routes. | | IP address | Not usable today. The caller's address is not propagated to your function — `x-forwarded-for` and `x-real-ip` both carry platform-internal addresses, and the first `x-forwarded-for` hop varies between requests, so each request would consume a different budget. | Avoid putting secrets or other sensitive values directly in a key. ## Behavior and limitations | Behavior | Detail | |---|---| | Per-site counters | Each Edge Compute site enforces its own budget. Requests served from different sites do not share a global counter. | | Fixed windows | Windows align to 10-second or 60-second clock boundaries. A client can use the end of one window and the start of the next, allowing a short burst of up to twice the configured limit. | | Platform-managed storage | Counters live in an internal per-site KV bucket and survive a function pod restart. It is not one of your KV namespaces — you never create, list, or delete it, and keys expire on their own. | | Request latency | An allowed check reads and updates the backing counter. A check already known to be over limit may be rejected from an in-process cache. Do not assume every call is network-free. | | Fails open | If the counter store cannot be reached, checks return `success: true` and requests are admitted. An outage means traffic is unlimited, not rejected. | | Concurrent first requests | Rate limiting is not a strict concurrency barrier. Simultaneous first checks for a previously unseen key can temporarily exceed the configured limit while its counter is initialized. Do not use this binding as the only control for hard financial, inventory, or security quotas. | | No sliding window | V0 supports fixed windows only. | | No global enforcement | V0 does not coordinate one budget across all sites. | ## Related resources - [Quick Start](/docs/edge-compute/rate-limiting/quick-start) — Configure, deploy, and test a rate limiter - [API reference](/docs/edge-compute/rate-limiting/api-reference) — Runtime method inputs, return values, and failure behavior - [Bindings](/docs/edge-compute/runtime/bindings) — How runtime resource handles are declared and resolved - [Configuration](/docs/edge-compute/configuration) — The complete `telnyx.toml` reference - [Deploy a function](/docs/edge-compute/deploy) — Ship configuration and code changes --- ### Quick Start > Source: https://developers.telnyx.com/docs/edge-compute/rate-limiting/quick-start.md This walkthrough configures a function to allow two requests per authenticated user during each 10-second window. Use `telnyx-edge` v0.4.0 or later. Earlier versions may upload an invalid `[[ratelimits]]` block instead of rejecting it locally, and do not generate the binding's type. ## 1. Start from a project This walkthrough uses a `telnyx.toml` project, where the runtime serves your `fetch` handler. `new-func --actor` scaffolds one: ```bash telnyx-edge new-func -l ts -n my-api --actor cd my-api npm install # pulls in @telnyx/edge-runtime ``` Every command below runs from the project directory. It works in a `func.toml` project too — keep your own HTTP server and reach the binding with `import { env } from "@telnyx/edge-runtime"`. ## 2. Declare a rate limiter Add a `[[ratelimits]]` block to `telnyx.toml`, alongside whatever else the project declares: ```toml name = "my-api" main = "src/index.ts" compatibility_date = "2026-05-01" [[actors]] binding = "COUNTER" type = "Counter" [[ratelimits]] name = "API_LIMIT" limit = 2 period = 10 [edge_compute] # written by new-func — leave it as generated func_id = "..." func_name = "my-api" ``` This configuration allows two successful checks per key in each 10-second window. ## 3. Check the limit in your handler Generate the binding's type first, so `env.API_LIMIT` is typed for you. It is written into `telnyx-env.d.ts` alongside every other binding the manifest declares, so a project that also hosts an actor gets both in one `Env`: ```bash telnyx-edge types # writes telnyx-env.d.ts; needs telnyx-edge v0.4.0+ and @telnyx/edge-runtime 0.9.2+ ``` ```ts // Keep the scaffold's actor export: telnyx.toml still declares type = "Counter", // and the runtime loads that class from this module. export { Counter } from "./counter"; export default { async fetch(request: Request, env: Env): Promise { // Only trust this header when an authenticated upstream sets it. const userId = request.headers.get("x-authenticated-user-id"); if (!userId) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } const { success } = await env.API_LIMIT.limit({ key: userId }); if (!success) { return Response.json( { error: "Rate limit exceeded" }, { status: 429, headers: { "Retry-After": "10" }, }, ); } return Response.json({ ok: true }); }, }; ``` The binding returns a decision; it does not send a `429` response automatically. ## 4. Ship and test ```bash telnyx-edge ship ``` For the configuration above, three requests with the same trusted user ID during one window produce two `200` responses followed by a `429`: ```bash curl -i -H 'x-authenticated-user-id: user-123' https:// curl -i -H 'x-authenticated-user-id: user-123' https:// curl -i -H 'x-authenticated-user-id: user-123' https:// ``` The CLI validates each declared limiter before upload and prints its name, limit, and period. See the [API Reference](/docs/edge-compute/rate-limiting/api-reference) for the binding's method contract and return values. --- ### API Reference > Source: https://developers.telnyx.com/docs/edge-compute/rate-limiting/api-reference.md Each `[[ratelimits]]` entry exposes a rate limiter binding on `env` — as the handler's `env` argument in a `telnyx.toml` project, or through `import { env } from "@telnyx/edge-runtime"` in a `func.toml` one. The binding name is uppercased and hyphens are replaced with underscores, so a limiter named `api-limit` is available as `env.API_LIMIT`. ## `env.NAME.limit({ key })` Checks and increments the counter for `key` in the current fixed window. ```ts const result = await env.API_LIMIT.limit({ key: "tenant-42:user-123" }); ``` ### Input | Input | Type | Description | |---|---|---| | `key` | string | Caller-supplied identifier whose budget is independent of every other key. | ### Return value The method returns `Promise<{ success: boolean }>`: | Value | Meaning | |---|---| | `{ success: true }` | The call was accepted. It consumes one unit from the key's budget, unless the counter store was unreachable — see fail-open below, where the request is admitted without being counted. | | `{ success: false }` | The key has reached its limit for the current window. | Treat `success: false` as a rejection. Rate limiting fails open: if the counter store is unreachable the check returns `{ success: true }` and the request is admitted, so an outage means requests are not limited rather than all being rejected. The binding returns only the decision. Your function is responsible for returning an appropriate response, such as HTTP `429 Too Many Requests`. ```ts const { success } = await env.API_LIMIT.limit({ key: userId }); if (!success) { return Response.json( { error: "Rate limit exceeded" }, { status: 429, headers: { "Retry-After": "10" }, }, ); } ``` ## TypeScript type `telnyx-edge types` writes the binding's declaration into `telnyx-env.d.ts`, so `env.` is typed for you. It requires `@telnyx/edge-runtime` 0.9.2 or later, the first release exporting `RateLimiter`. The generated shape is: ```ts interface RateLimiter { limit(options: { key: string }): Promise<{ success: boolean }>; } interface Env { API_LIMIT: RateLimiter; } ``` See [Rate Limiting](/docs/edge-compute/rate-limiting) for configuration, usage patterns, and platform behavior. --- ## Observability ### Observability > Source: https://developers.telnyx.com/docs/edge-compute/observability.md Edge Compute has no customer-facing telemetry surface today: there is no `telnyx-edge logs` command, no log dashboard, and no metrics or traces. `console.log` output from a running function is not readable anywhere. What you can observe is built from three things — the CLI's control-plane views, your function's own health endpoint, and structured events your function emits over HTTPS to a collector you run. ## Control-plane visibility The CLI answers "is it deployed, and where does it answer" — not "what is it doing": | Command | What it tells you | | ------- | ----------------- | | `telnyx-edge ship` | Build and deploy progress for one revision, ending in the live URL — build and deploy failures surface here | | `telnyx-edge list` | Every function: id, name, status, creation time, invoke URL | | `telnyx-edge inspect ` | One function's status, invoke URL, timestamps, and actor bindings — accepts a name or an id | | `telnyx-edge revisions list ` | Deploy history, newest first — each successful ship is an immutable revision you can [roll back to](/docs/edge-compute/configuration/versions) | | `telnyx-edge status` | CLI self-diagnostics: config file, authentication, connectivity to `api.telnyx.com` — it checks your CLI, not your functions | Add `-v` to any command for verbose client-side logging when a command itself misbehaves. None of this shows requests, errors, or output from the running container. For that, read on. ## Health checks The scaffolded TypeScript and JavaScript entrypoints answer `/health` before any other routing: ```ts // index.ts — the scaffold's fast path if (req.url === '/health' || req.url?.startsWith('/health/')) { res.writeHead(200); res.end(); return; } ``` Keep this route dependency-free — no KV reads, no outbound calls — so an external checker can tell "function down" apart from "dependency down". The Quarkus scaffold serves `/health` through SmallRye Health; in Go and Python, add an equivalent route yourself. HTTP is the only trigger, so probing is external by design: point an uptime monitor — or a scheduled job such as a GitHub Actions cron — at `https://-.telnyxcompute.com/health`. ## Emit events to a sink you run Since nothing shows you a running function's output, the pattern is to send structured events over HTTPS to a collector you control — any log store with an HTTP ingest endpoint works. Store the collector's credential as a [secret](/docs/edge-compute/configuration/secrets), never in code. Declare the secret in `func.toml` and store its value: ```toml [[secrets]] binding = "LOG_SINK_KEY" # the handle you pass to env.SECRETS.get() name = "LOG_SINK_KEY" # the secret's name from `secrets add` ``` ```bash telnyx-edge secrets add LOG_SINK_KEY "" telnyx-edge types # regenerate telnyx-env.d.ts so the handle type-checks ``` Then instrument the entrypoint. This emits one event per request — off the critical path, with a deadline, and never able to fail the response: ```ts import * as http from "node:http"; import { randomUUID } from "node:crypto"; import { env } from "@telnyx/edge-runtime"; const SINK_URL = "https://logs.example.com/ingest"; // your collector function emit(event: Record): void { // Fire-and-forget: telemetry must never delay or fail a response. void (async () => { const key = await env.SECRETS.get("LOG_SINK_KEY"); await fetch(SINK_URL, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` }, body: JSON.stringify({ ts: new Date().toISOString(), ...event }), signal: AbortSignal.timeout(2000), // a slow sink must not pile up sockets }); })().catch(() => {}); // a dead sink must not take the function with it } const server = http.createServer((req, res) => { if (req.url === "/health" || req.url?.startsWith("/health/")) { res.writeHead(200); res.end(); return; } const requestId = (req.headers["x-request-id"] as string) ?? randomUUID(); const start = Date.now(); res.setHeader("X-Request-ID", requestId); res.on("finish", () => { emit({ level: res.statusCode < 500 ? "info" : "error", requestId, method: req.method, path: req.url, status: res.statusCode, durationMs: Date.now() - start, }); }); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true, requestId })); }); server.listen(process.env.PORT || 8080); ``` What makes this pattern hold up: - **Propagate a request id.** Read `X-Request-ID` or generate one, return it in the response, and attach it to every event — a user-reported failure becomes findable in your sink. - **Log metadata, not payloads.** Method, path, status, duration. Never secret values, and not full request bodies, which may carry PII. - **Buffering trades loss for volume.** The per-request emit above is the simple, safe default. If volume demands batching, remember events buffered in memory are gone when the container stops — see [Execution model](/docs/edge-compute/runtime/execution-model) for the container lifecycle. The `env.SECRETS` binding is TypeScript-only, but secrets are also injected as plain environment variables into every function, so the same pattern works in any runtime: read the key from the environment (`os.Getenv("LOG_SINK_KEY")` in Go, `os.environ` in Python) and POST JSON to your sink. ## Next Steps - [Best Practices](/docs/edge-compute/best-practices) — error handling and outbound-call deadlines the emitter should respect - [Limits](/docs/edge-compute/platform/limits) — the 30 s default / 60 s max request budget your telemetry lives inside - [Secrets](/docs/edge-compute/configuration/secrets) — both access surfaces for the sink credential - [CLI Reference](/docs/edge-compute/reference/cli) — full flags for `list`, `inspect`, `status`, and `revisions` --- ## CLI ### CLI Reference > Source: https://developers.telnyx.com/docs/edge-compute/reference/cli.md `telnyx-edge` is the command-line tool for Edge Compute: it scaffolds function projects, deploys them, and manages the resources they bind. This page covers every command in v0.2.5. | Command | What it does | |---------|--------------| | [auth](#auth) | Log in via OAuth or API key; check or clear credentials | | [new-func](#new-func) | Scaffold a project and register the function server-side | | [ship](#ship) | Upload, build, and deploy a function | | [list](#list) | List your functions with status and invoke URL | | [inspect](#inspect) | One function's full details and actor bindings | | [status](#status) | CLI self-diagnostics: config, auth, connectivity | | [revisions](#revisions) | A function's deploy history | | [rollback](#rollback) | Retarget traffic to a previous revision | | [secrets](#secrets) | Manage organization-scoped secrets | | [bindings](#bindings) | Manage the org-level Telnyx API credential | | [types](#types) | Generate `telnyx-env.d.ts` from the project manifest | | [storage](#storage) | Manage KV namespaces and keys, and SQL databases | | [actors](#actors) | Manage account-scoped Stateful Actor types | | [reset-func](#reset-func) | Return a failed function to the `created` state | | [delete-func](#delete-func) | Delete a function permanently | ## Installation The CLI ships as GitHub release binaries only — it is not on npm and there is no Homebrew formula. Assets are version-stamped; there is no un-versioned "latest" asset (`releases/latest/download/...` URLs return 404). | Platform | Asset | |----------|-------| | Linux amd64 | `telnyx-edge-v0.2.5-linux-amd64.tar.gz` | | Linux arm64 | `telnyx-edge-v0.2.5-linux-arm64.tar.gz` | | macOS arm64 (Apple silicon) | `telnyx-edge-v0.2.5-macos-arm64.tar.gz` | | macOS amd64 (Intel) | `telnyx-edge-v0.2.5-macos-amd64.tar.gz` | | Windows | `.zip` archives on the same [release page](https://github.com/team-telnyx/edge-compute/releases) | Each tarball extracts into a versioned directory containing the `telnyx-edge` binary: ```bash VERSION=v0.2.5 curl -fsSL "https://github.com/team-telnyx/edge-compute/releases/download/${VERSION}/telnyx-edge-${VERSION}-linux-amd64.tar.gz" | tar xz sudo mv "telnyx-edge-${VERSION}-linux-amd64/telnyx-edge" /usr/local/bin/ telnyx-edge --version # prints the installed version ``` For macOS, substitute `macos-arm64` (Apple silicon) or `macos-amd64` (Intel) in both lines. To update, download the new version's asset and replace the binary the same way. ## Global flags and configuration | Flag | Effect | |------|--------| | `-h`, `--help` | Help for any command: `telnyx-edge --help` | | `-v`, `--verbose` | Verbose logging — the first thing to try on an obscure failure | | `--version` | Print the CLI version (root command only) | Credentials persist in `~/.telnyx-edge/config.toml`. Two environment variables affect the binary itself: `TELNYX_CONFIG_PATH` relocates the config file, and `TELNYX_NO_UPDATE_CHECK` disables the release update check. ## auth ```bash telnyx-edge auth login # OAuth 2.0 in the browser telnyx-edge auth api-key set "KEY..." # persist a Telnyx API key instead telnyx-edge auth status # who am I, and does the token work telnyx-edge auth logout # clear stored tokens ``` `login` opens a browser for OAuth; `api-key set` writes the key to `~/.telnyx-edge/config.toml`. Both end in the same place — subsequent commands read the stored credential. The CLI does not read a `TELNYX_API_KEY` environment variable. In CI, run `telnyx-edge auth api-key set "$TELNYX_API_KEY"` as a pipeline step — see [CI/CD](/docs/edge-compute/deploy). ## new-func ```bash telnyx-edge new-func -l ts -n my-func cd my-func ``` | Flag | Description | |------|-------------| | `-l`, `--language` | Runtime — exactly one of `go`, `js`, `ts`, `python`, `quarkus`. **Required** unless `--from-dir` is given; the value is exact (`javascript` is rejected). | | `-n`, `--name` | Function name. Becomes the directory name and part of the URL. | | `--actor` | Scaffold a Stateful Actor (`telnyx.toml`) project — TypeScript only. See the [Stateful Actors quick start](/docs/edge-compute/stateful-actors/quick-start). | | `--from-dir` | Copy files from an existing directory instead of a language scaffold. | `new-func` does two things: it creates a project directory (the command fails if one with that name already exists), and it **registers the function server-side** — so it requires authentication, and the generated `func.toml` already contains the function's UUID `func_id`. Rapid successive calls can hit HTTP 429 rate limits. What each scaffold contains: | Language | Files | |----------|-------| | `ts` | `func.toml`, `index.ts`, `package.json`, `tsconfig.json` | | `js` | `func.toml`, `index.js`, `package.json` | | `go` | `func.toml`, `handler.go`, `go.mod` | | `python` | `func.toml`, `function/func.py`, `pyproject.toml` | | `quarkus` | `func.toml`, `pom.xml`, `mvnw`, `.mvn/`, `src/main/java/functions/` | The entrypoint contract differs per language — see [HTTP handler](/docs/edge-compute/runtime/http-handler). ## ship ```bash telnyx-edge ship # deploy the function in the current directory telnyx-edge ship --from-dir ../other # or any relative, absolute, or ~/ path ``` | Flag | Description | |------|-------------| | `-f`, `--from-dir` | Path to the function directory (default: current directory) | | `-t`, `--timeout` | Deployment monitoring timeout as a Go duration (`2m`, `300s`; default `5m0s`) | `ship` uploads, builds, pushes, and deploys the function named by the directory's `func.toml`. There is no environment flag — staging and production are [separate functions](/docs/edge-compute/deploy#staging-and-production). Umbrella projects (`telnyx.toml`) are bundled client-side before upload: the module graph rooted at `main` is compiled into a single file with esbuild (TypeScript/JavaScript only), and the manifest is included so the platform can deploy any `[[actors]]` it declares. On success, `ship` prints the live URL — stable across deploys: ``` 📡 Your function is live at: https://my-func-0198c2c5-8.telnyxcompute.com ``` The scheme is `{func-name}-{func-id-prefix}.telnyxcompute.com` — see [Routes & Domains](/docs/edge-compute/configuration/routing). Each successful ship also produces an immutable revision ([revisions](#revisions), [rollback](#rollback)). ## list ```bash telnyx-edge list telnyx-edge list --page 2 --page-size 50 ``` Lists your functions — id, name, status, creation time, and invoke URL. Paginated: `--page` (default 1) and `--page-size` (default 25). ## inspect ```bash telnyx-edge inspect my-func # accepts a name or an id (first column of 'list') ``` Shows one function's status, invoke URL, and timestamps, plus the actor types it binds — each binding's type, status, and owner/reference role. ## status ```bash telnyx-edge status ``` Self-diagnostics: config file existence, authentication status, and connectivity to `https://api.telnyx.com`. Run it first when any other command misbehaves. ## revisions ```bash telnyx-edge revisions list my-func ``` Lists the most recent revisions for a function, newest first, with each revision's id, ship time, author, and deploy status; the revision currently serving traffic is marked. Every successful `ship` produces an immutable revision — see [Versions & Rollback](/docs/edge-compute/configuration/versions). ## rollback ```bash telnyx-edge rollback my-func a1b2c3d # → Rollback of 'my-func' to revision a1b2c3d accepted; traffic is switching across clusters. ``` Instantly retargets traffic to an existing, immutable revision across all clusters — no rebuild, no re-upload. Only revisions that reached `deploy_ok` can be rolled back to; get ids from `revisions list`. Your source tree is untouched — the next `ship` deploys whatever is on disk, as a new revision. ## secrets Secrets are organization-scoped key-value pairs for sensitive data. The arguments are positional — there are no `--name`/`--value` flags: ```bash telnyx-edge secrets add STRIPE_API_KEY "sk_live_abc123" # → Secret 'STRIPE_API_KEY' added successfully telnyx-edge secrets list # keys only — values are never shown telnyx-edge secrets delete OLD_API_KEY ``` `add` on an existing key overwrites it. Values are injected at deploy time, so `ship` each function that uses a changed secret. Functions read secrets two ways, and both are always true: every secret is injected as a plain environment variable into **all** functions in your organization, and TypeScript functions can additionally declare a `[[secrets]]` binding and read through the typed `env.SECRETS.get()`. See [Secrets](/docs/edge-compute/configuration/secrets) for both surfaces. ## bindings Manages the **org-level Telnyx credential** (one per organization) behind the [Telnyx API binding](/docs/edge-compute/telnyx-api). The per-function flow needs none of these commands — declaring `[telnyx]` in `func.toml` wires the binding automatically on `ship`. ```bash telnyx-edge bindings create # provision the org credential (one per organization) telnyx-edge bindings get # binding metadata telnyx-edge bindings validate # check the credential works telnyx-edge bindings update # regenerate — use if you suspect compromise telnyx-edge bindings delete # remove; functions lose automatic Telnyx API access ``` ## types ```bash telnyx-edge types # writes telnyx-env.d.ts at the project root telnyx-edge types -f ./my-func # or point at another project directory ``` Generates TypeScript types for the `env` surface from your manifest (`func.toml` or `telnyx.toml`), folding every declared binding into one global `Env` interface: | Declaration | Generated type | |-------------|----------------| | `[telnyx]` | `env.` is the Telnyx client class from the `telnyx` npm package — `env..balance.retrieve()` type-checks | | `[[secrets]]` | `env.SECRETS.get()` accepts the literal union of declared handles → `Promise`; a typo'd handle fails to compile | | `[storage.kv.]` | `env.` is `KvNamespace` from `@telnyx/edge-runtime` — new in v0.2.3 | | `[storage.cloudstorage.]` | `env.` is `CloudStorageBucket` from `@telnyx/edge-runtime` — new in v0.2.4 | | `[storage.sqldb.]` | `env.` is `SqlDatabase` from `@telnyx/edge-runtime` — requires the SDK at 0.9.0 or newer | | `[[actors]]` | `env.` exposes the bound actor class's public method signatures (umbrella `telnyx.toml` projects only) | | `[[ratelimits]]` | `env.` is `RateLimiter` from `@telnyx/edge-runtime` — new in v0.4.0, requires the SDK at 0.9.2 or newer | Declarations only — no JavaScript, no runtime glue, no source edits. Re-run after changing any binding declaration. `types` generates a `.d.ts` consumed by `tsc` — it has no effect on `js`, `go`, `python`, or `quarkus` runtimes. Bindings on those runtimes are reached over REST instead; see [Bindings](/docs/edge-compute/runtime/bindings). ## storage ```bash telnyx-edge storage kv create --name my-cache telnyx-edge storage kv key put user/123 "hello" --ttl 30s telnyx-edge storage sqldb create --name links-db telnyx-edge storage sqldb execute links-db --remote --command "SELECT 1" ``` Manages KV storage namespaces and keys: `storage kv` covers namespace create/list/get/delete, and `storage kv key` covers put/get/list/delete including server-side TTL and prefix listing. Full flags and examples live in the [KV CLI reference](/docs/edge-compute/kv/cli). `storage sqldb` manages SQL databases: create/list/get/delete, plus `execute` for running SQL against a database out-of-band and `migrations` for versioned schema files. It arrives in a later release than the v0.2.5 covered above — see the [SQL Databases CLI reference](/docs/edge-compute/sqldb/cli) for the version floor, full flags, and examples. ## actors ```bash telnyx-edge actors list # the account's registered actor types telnyx-edge actors inspect Account # one type, its attached functions, + live instance count telnyx-edge actors instances Account # list persisted instances (type/id pairs) telnyx-edge actors delete Account # delete an account-scoped type ``` Inspects and manages the [Stateful Actor](/docs/edge-compute/stateful-actors) types registered to your account (account-scoped, keyed by type). `inspect` reports the actor type's live instance count; `instances` lists the persisted instances (type/id pairs, e.g. `Counter/alice`). Output renders backend state — never inferred from local files. ## reset-func ```bash telnyx-edge reset-func broken-func ``` Tears down a failed function's deployed resources and returns it to the `created` state — preserving its id, name, and config — so you can fix the code and `ship` again. Allowed only from a terminal failure state (`build_failed`, `deploy_failed`, `delete_failed`); a healthy function can't be reset (use `delete-func`), and an in-progress operation must finish first. ## delete-func ```bash telnyx-edge delete-func my-old-func ``` Deletes a function by name. This cannot be undone — the function, its revisions, and its URL are gone. ## Related - [Configuration](/docs/edge-compute/configuration) — every `func.toml` / `telnyx.toml` key the CLI reads - [CI/CD](/docs/edge-compute/deploy) — install, authenticate, and ship from a pipeline - [Versions & Rollback](/docs/edge-compute/configuration/versions) — how revisions and rollback behave - [KV CLI](/docs/edge-compute/kv/cli) — the full `storage kv` surface - [SQL Databases CLI](/docs/edge-compute/sqldb/cli) — the full `storage sqldb` surface, including `execute` and `migrations` - [Stateful Actors](/docs/edge-compute/stateful-actors) — the projects behind `--actor` and the `actors` command --- ## Platform ### Pricing > Source: https://developers.telnyx.com/docs/edge-compute/platform/pricing.md Functions bill on two meters: **requests** (each HTTP request your function handles) and **CPU time** (metered in milliseconds). These are the only two meters — there is no charge for deploying, for the number of functions you keep, or for idle functions. ## Functions | Resource | Free tier | Paid | |----------|-----------|------| | Requests | 3.6M/month | $0.21/million | | CPU time | 36M ms/month | $0.014/million ms | ## Storage Storage is billed separately from function execution: - **KV** bills per operation and per GB-month stored — see [KV Pricing](/docs/edge-compute/kv/pricing). ## Related - [Limits](/docs/edge-compute/platform/limits) — execution, size, and rate limits - [KV Pricing](/docs/edge-compute/kv/pricing) — operation and storage rates for KV --- ### Limits > Source: https://developers.telnyx.com/docs/edge-compute/platform/limits.md Limits are behavioral contracts: each one states what the platform enforces, what you see when you hit it, and what to do instead. ## Execution Limits | Limit | Default | Maximum | |-------|---------|---------| | Request timeout | 30 seconds | 60 seconds | | Memory per container | 256 MB | 512 MB | | Request body size | 10 MB | 10 MB | | Response body size | 10 MB | 10 MB | **Request timeout.** A function must respond within the timeout or the request is terminated with a `504 Gateway Timeout`. The timeout is not set in `func.toml` — there is no `timeout_seconds` field. Budget your own upstream calls below the platform limit (for example, a 25-second timeout on outbound requests) so you can return a real error instead of a `504`. **Memory.** Each container has a fixed allocation. Exceed it and the container is terminated; the next request starts a fresh one (a cold start). Stream large payloads instead of buffering them, and don't let in-memory caches grow unbounded — memory is per-container and disappears on restart anyway. For durable state, see [Where state lives](/docs/edge-compute/runtime/execution-model#where-state-lives). ## Function Limits | Limit | Value | |-------|-------| | Function code size | 50 MB (compressed) | | Environment variables per function | 64 | | Environment variable name size | 256 bytes | | Environment variable value size | 5 KB | | Secrets per organization | 100 | | Secret value size | 10 KB | The code size limit includes dependencies after compression. If you hit it: remove unused dependencies, exclude development dependencies from the shipped directory, or split into multiple functions. ## Network Limits | Limit | Value | |-------|-------| | Outbound connections per request | 100 | | Outbound request timeout | Set by your code, bounded by the request timeout | | DNS resolution timeout | 5 seconds | The outbound connection count covers HTTP/HTTPS requests, database connections, and TCP sockets. Since a container serves many requests over its lifetime, open clients and connection pools once at module scope and reuse them across requests rather than reconnecting per invocation. ## Rate Limits | Limit | Value | |-------|-------| | Deployments per hour | 60 | | API requests per minute | 1,000 | | Concurrent function invocations | No hard limit (auto-scales) | Function creation is also rate limited: `telnyx-edge new-func` registers the function server-side at scaffold time, and rapid successive calls return `429`. Wait and retry. There is no hard cap on concurrent invocations — the platform scales containers with traffic. Each new container pays a cold start, so sharply spiky traffic sees higher tail latency. ## Account Limits | Limit | Value | |-------|-------| | Functions per organization | 100 | Total requests and CPU time are usage-billed with a monthly free tier — see [Pricing](/docs/edge-compute/platform/pricing). Need higher limits? Contact [support@telnyx.com](mailto:support@telnyx.com). ## KV Storage Limits | Limit | Value | |-------|-------| | Key length | 256 characters — over returns `400` | | Key characters | `a-z` `A-Z` `0-9` `-` `_` `/` `=` `.` (no colons) | | Value size | 1 MiB (1,048,576 bytes) — over returns `413` | | TTL | Whole seconds, minimum 1 | [KV Best Practices](/docs/edge-compute/kv/best-practices) is the authoritative KV limits page. For values over 1 MiB, store the object in [Cloud Storage](/docs/cloud-storage/quick-start) and keep a reference in KV. ## When a Limit Is Exceeded | Error | Code | Meaning | |-------|------|---------| | Request Timeout | 504 | Function didn't respond in time | | Memory Exceeded | 500 | Container terminated due to memory | | Payload Too Large | 413 | Request/response body exceeded limit | | Rate Limited | 429 | Too many requests or deployments | ## Related - [Pricing](/docs/edge-compute/platform/pricing) — free tier and usage rates - [Execution Model](/docs/edge-compute/runtime/execution-model) — container lifecycle and cold starts - [KV Best Practices](/docs/edge-compute/kv/best-practices) — full KV limits and guidance ---