Skip to main content

Telnyx Compute — Full Documentation

Edge compute functions, runtime bindings, stateful actors, and private networking. Complete page content for the Compute section of the Telnyx developer docs (https://developers.telnyx.com). Root index: https://developers.telnyx.com/llms.txt · Lightweight index for this section: https://developers.telnyx.com/development/llms/compute-llms-txt.md

Overview

Edge Compute

Source: https://developers.telnyx.com/docs/edge-compute/platform-overview.md
Edge Compute is the platform of compute primitives you use to build and deploy applications to the Telnyx edge — serverless functions, stateful actors, KV, object storage, and SQL (coming soon). It’s particularly powerful when combined with two other sets of Telnyx primitives to create AI agents that act in the physical world:

Start building


Functions

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.
index.ts
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 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. 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.

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 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. Assets are version-stamped, and each tarball extracts into a versioned directory containing the binary.
Linux (amd64)
macOS (Apple silicon)
For Intel Macs use the macos-amd64 asset; Windows zips are on the same releases page. Verify the install:

2. Authenticate

Authenticate before creating a function — new-func registers the function with the platform, which requires credentials.
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:
TypeScript
JavaScript
Go
Python
Java
Every scaffold contains a func.toml that ties the directory to the registered function:
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:
index.ts
index.js
handler.go
function/func.py
src/main/java/functions/Function.java
These are trimmed for the quickstart. HTTP handler has the full scaffold for each language, the exact entrypoint contract, and how request bodies and health probes work.

4. 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:
Every function gets a URL of the form {func-name}-{org-nickname}.telnyxcompute.com — see Routes & Domains.

5. Call it

Use the URL ship printed. Every scaffold answers a GET with the same default response:
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 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.

Next Steps

  • Bindings — pre-authenticated Telnyx client, secrets, and KV on env.
  • KV quick start — persist data across requests from your function.

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:
Every secret is injected into your functions as a plain environment variable, so this works in any language:
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.

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), and split work that genuinely needs longer. Exact numbers: Limits.

Name Functions for Their URL

The function name becomes the hostname — {func-name}-{org-nickname}.telnyxcompute.com — and new-func registers the function with the platform at scaffold time, so pick the name up front:

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:
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:
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.

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: 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 exist for. Full comparison: 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; a check-then-act on KV can race.

Keep the Health Endpoint Fast

The scaffolds answer /health before any other routing:
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:

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:
Retry only network failures and 5xx responses, with exponential backoff, and keep the total budget under the platform timeout:

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, 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.

Next Steps

  • Limits — the exact numbers behind timeouts, memory, and payload sizes
  • Observability — building your own telemetry with no platform logging surface
  • Execution Model — container lifecycle, scaling, and cold starts

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


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). 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:
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.

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

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:
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
handler.go

Step 4: Ship the function

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.
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 or via the API: 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:

Webhook tool

Add a webhook tool that points to the same function URL:
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):
  2. Make a test call to the assistant from the Portal or via the API:
  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 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:

Next steps

  • Dynamic Variables — full reference for the DV webhook payload and resolution precedence.
  • Webhook signing — how Telnyx signs webhooks and how to verify signatures.
  • Edge Compute quickstart — getting started with your first function.
  • Secrets — encrypted, org-scoped environment variables.
  • Bindings — pre-authenticated Telnyx API client for your function.

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); the Python and Go serve rows add a small local entry point shown in their tabs:

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:
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:
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:
--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.
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:
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:
The scaffold is a Quarkus Funqy project. Dev mode gives hot reload on source changes and a debugger on port 5005:
The test stack (quarkus-junit5, rest-assured) is already in the pom.xml:

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 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[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 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:
Use throwaway values locally and never commit real ones. See Secrets and Environment variables.

Deploy

When it works locally, ship it:
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.

Next Steps

  • Deploy — ship, revisions, and rollback
  • HTTP handler — the exact entrypoint contract per language
  • Bindings — the env surface a deployed function gets
  • CLI reference — every telnyx-edge command

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.], and [storage.cloudstorage.].
  • telnyx.toml (umbrella) — a 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 binding declaration so telnyx-env.d.ts matches the manifest. The binding blocks — [telnyx], [[secrets]], [storage.kv.], [storage.cloudstorage.], and [[actors]] (umbrella only) — each resolve to a typed handle on env. This page documents their manifest keys; the 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:
A manifest using every available block:
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).

[edge_compute] — identity

[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.

[[secrets]] — secret bindings

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.

[telnyx] — Telnyx API binding

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.

[storage.kv.] — KV namespace binding

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.

[storage.cloudstorage.] — Cloud Storage bucket binding

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.

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.
The project shape — one module exporting both the actor class and a fetch handler — is covered in Project Structure. telnyx-edge new-func --actor scaffolds it. Documented in Stateful Actors — the configuration reference covers the [[actors]] block in full.

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.
  • KV namespaces — created with telnyx-edge storage kv create; [storage.kv.] only references the namespace id. See the KV quick start.
  • Cloud Storage buckets — created in the Mission Control portal or over the S3-compatible API; [storage.cloudstorage.] only references an existing bucket by bucket_name. See Cloud Storage binding.

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 rejectedship 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.

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

Declaring variables

Define non-sensitive configuration under [env_vars] in func.toml:
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 instead.

Reading variables

Environment variables vs secrets

Next Steps

  • Secrets — the server-side counterpart for sensitive values, including the typed env.SECRETS surface for TypeScript
  • Bindings — typed, pre-authenticated handles instead of raw variables
  • 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:
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:

Through the typed binding — TypeScript

TypeScript projects can additionally declare a [[secrets]] binding in func.toml and read the secret through env.SECRETS:
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 for how the env namespace works.

Rotating a secret

add with an existing key overwrites its value:

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:

Troubleshooting

Next Steps

  • Environment variables — the full picture of what lands in your container’s environment
  • Bindings — the declare → typesenv pattern all bindings share
  • Configuration[[secrets]] and every other manifest key

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

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.
On success, ship prints the function’s live URL (https://{func-name}-{org-nickname}.telnyxcompute.com — see Routes & Domains). 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.
Add the secret under Settings → Secrets and variables → Actions → New repository secret, named TELNYX_API_KEY.

GitLab CI

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

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):
Keep one codebase and both generated func.toml files in the repo; each pipeline job copies the matching one into place before shipping:
Because bindings are declared in func.toml, the two files can also point at per-environment resources — for example a separate KV namespace id per environment.
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:
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:
If it fails, roll back with telnyx-edge rollback as above. There is no platform metrics or logs surface to poll — see Observability for what your function should emit instead.

CI secrets vs. function secrets

Two different things: 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.

Troubleshooting

Next Steps


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

A function named hello-world in the organization acme is served at:
telnyx-edge ship prints the URL after a successful deploy:
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):
Requests time out after 30 seconds by default (60 seconds maximum) — see 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


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

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

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:
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 — ship from a pipeline; rollback is your escape hatch
  • Routes & Domains — the function URL always points at the active revision
  • CLI referenceship, revisions, rollback, and reset-func in full

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 or a bucket.
  • 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 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: The exact per-language contract — files, signatures, health probes, bodies, the request budget — is specified in 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 documents every binding type and how to declare it; Environment variables and 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).
  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:
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 vars 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.

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), 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 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.

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 for the full table. Budget outbound calls below the deadline so you return a real error instead of being cut off:

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 and Handling calls. For periodic work, call the URL from an external scheduler (a GitHub Actions cron job is enough), or use a Stateful Actor alarm 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: 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 — the entrypoint contract per language
  • Limits — timeouts, memory, and payload caps
  • 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) 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

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):
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.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):
function/__init__.py re-exports the factory: from .func import new. 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:
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:
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:
  • 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.

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.
  • Bindings — the typed env surface: Telnyx API, secrets, KV
  • Execution model — lifecycle, cold starts, concurrency
  • 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 func.toml 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.

Every binding works the same way

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

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.], and [storage.cloudstorage.].
  • telnyx.toml (umbrella) — a manifest with top-level name and main. Declares the same bindings, plus [[actors]] — actor classes are imported from main, which is why actors require the umbrella form.

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.
  • 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 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 with your own access keys.
  • Stateful Actors — TypeScript only; there is no REST fallback today.

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 for your own third-party credentials.

Next Steps


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.
  • Free-formed nameMY_TELNYX is whatever you set as binding in func.toml. It becomes the property on env.
  • Typedtelnyx-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. The org-level credential behind the binding (bindings create / validate / update) is account-level and rarely touched — see the CLI reference.

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

2. Declare the binding

Add a [telnyx] block to the generated func.toml:

3. Generate types

env.MY_TELNYX is now typed as the Telnyx client.

4. Write the handler

5. Ship

6. Call it

ship prints your function URL. Hit it:

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:
Each method returns the API response; list and retrieve calls expose the payload on .data.

Errors

Calls reject on API errors. Catch and inspect:

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

Declare the binding in func.toml (see the Quick start):

2. 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:

4. Test on-net

Send from another Telnyx number on your account to your function’s number:
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:
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:
This handles the STOP message itself but doesn’t remember it — store opted-out numbers in 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

Declare the binding in func.toml:

2. Ship

3. Point a Call Control app at it

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:
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.

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”: 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:
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, never in code. Declare the secret in func.toml and store its value:
Then instrument the entrypoint. This emits one event per request — off the critical path, with a deadline, and never able to fail the response:
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 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 — error handling and outbound-call deadlines the emitter should respect
  • Limits — the 30 s default / 60 s max request budget your telemetry lives inside
  • Secrets — both access surfaces for the sink credential
  • CLI Reference — full flags for list, inspect, status, and revisions

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.

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). Each tarball extracts into a versioned directory containing the telnyx-edge binary:
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

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

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.

new-func

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: The entrypoint contract differs per language — see HTTP handler.

ship

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. 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:
The scheme is {func-name}-{org-nickname}.telnyxcompute.com — see Routes & Domains. Each successful ship also produces an immutable revision (revisions, rollback).

list

Lists your functions — id, name, status, creation time, and invoke URL. Paginated: --page (default 1) and --page-size (default 25).

inspect

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

Self-diagnostics: config file existence, authentication status, and connectivity to https://api.telnyx.com. Run it first when any other command misbehaves.

revisions

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.

rollback

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:
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 for both surfaces.

bindings

Manages the org-level Telnyx credential (one per organization) behind the Telnyx API binding. The per-function flow needs none of these commands — declaring [telnyx] in func.toml wires the binding automatically on ship.

types

Generates TypeScript types for the env surface from your manifest (func.toml or telnyx.toml), folding every declared binding into one global Env interface: 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.

storage

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.

actors

Inspects and manages the Stateful Actor 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

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

Deletes a function by name. This cannot be undone — the function, its revisions, and its URL are gone.
  • Configuration — every func.toml / telnyx.toml key the CLI reads
  • CI/CD — install, authenticate, and ship from a pipeline
  • Versions & Rollback — how revisions and rollback behave
  • KV CLI — the full storage kv surface
  • Stateful Actors — the projects behind --actor and the actors command

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

Storage

Storage is billed separately from function execution:
  • KV bills per operation and per GB-month stored — see KV Pricing.
  • Limits — execution, size, and rate limits
  • 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

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.

Function Limits

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

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

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

Total requests and CPU time are usage-billed with a monthly free tier — see Pricing. Need higher limits? Contact support@telnyx.com.

KV Storage Limits

KV Best Practices is the authoritative KV limits page. For values over 1 MiB, store the object in Cloud Storage and keep a reference in KV.

When a Limit Is Exceeded


Stateful Actors (Beta)

Stateful Actors

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors.md
A stateful actor is a single-threaded server that owns the state of one entity — one user, one cart, one call leg, one chat room. You write it as a TypeScript class; the platform runs one instance per name, routes every call for that name to that instance, runs your methods one at a time, and persists what you write. You never bind a socket, take a lock, or shard it.
That debit is a read-modify-write with no lock and no transaction. In a normal request handler that’s a race; here it isn’t — and that property is the whole product. How it works derives it from first principles, starting from a plain C server. Two words used precisely throughout these docs: the actor is the class you ship (Account); an instance is its per-name materialization (idFromName("acct_123") reaches the acct_123 instance of Account).

What the Runtime Guarantees

  • One instance per nameidFromName("acct_123") routes every call to a single owner; no two hosts run it at once.
  • One call at a time — single-threaded dispatch. No locks, no races inside an instance.
  • Durable before reply — a method’s result isn’t returned until the writes it made are persisted.
  • Memory is a cache — only what you write to storage survives eviction or restart.
Execution model states each precisely; How it works derives them from a plain C server. Scaffold, deploy, and curl an actor. The C-vs-actor derivation from first principles. The four guarantees, stated precisely. Where instances run; what survives a restart. Naming and routing instances. The two-export shape. Reach one actor from many functions. And when to reach for something else. The full surface. Scheduled work from inside an actor. Terminate a live socket on the actor — each frame is a serialized turn.
  • Bindings — how bindings resolve on env
  • KV — globally distributed key-value storage
  • Execution Model — function lifecycle and concurrency

Stateful Actors Quick Start

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/quick-start.md
Ship an Account actor end-to-end: scaffold, write the class, deploy, and curl it. Each account name is its own actor instance — alice and bob are isolated, each with their own balance, all persisted. Roughly 10 minutes.

What You’ll Build

A function with routes that all share a single durable Account actor type — but each account name is its own isolated instance:
A debit never overdraws, and a committed debit is never lost.

Why an Account

An Account exercises the three properties a Stateful Actor gives you:
  • Per-instance isolationalice and bob are separate actor instances with their own balance.
  • Single-threaded dispatch — concurrent debits on the same account serialize automatically. No lock, no race: two debits can’t both pass the balance check.
  • Persistence — the balance survives across requests; it’s durable and reloaded when the actor is next invoked.

Prerequisites

  • The telnyx-edge CLI, installed and authenticated — a release with Stateful Actor deploy support. v0.2.2 and earlier cannot ship an actor project (new-func --actor appears to work, but ship fails); upgrade from the releases page.
  • A Telnyx API key.
  • Node.js (for npm).

1. Authenticate

2. Scaffold the Actor Project

This writes an umbrella telnyx.toml (with a [[actors]] block), a sample actor class, a fetch handler, and registers the function (writes a func_id into telnyx.toml). npm install pulls in @telnyx/edge-runtime.

3. Write the Account Actor

Replace the scaffolded actor class with src/account.ts. It holds one value — the balance — in this.ctx.storage:

4. Write the Fetch Handler

Replace src/index.ts with a handler that routes HTTP to actor method calls. It re-exports the actor class (so it ships with the bundle) and calls it through the ACCOUNT binding on env:

5. Update telnyx.toml

Point the [[actors]] binding at your class. The scaffold generated a sample binding; change it to ACCOUNTAccount:

6. Deploy

ship bundles the project, uploads it, and monitors the deploy — wait for ✅ Func 'account' is now deployed! (in telnyx-edge list, the function’s status becomes deploy_ok).

7. Hit the URL

ship prints a URL like account-.telnyxcompute.com. The function may take a moment to come up — poll the health endpoint until it returns 200:
Now exercise it — two accounts, no overdraw, isolation, persistence:
You should see:
  • alice ends at 70 (100 − 30; the 1000 debit was rejected)
  • bob is independent at 5
  • alice is unchanged by bob’s activity (isolation), and survives across requests (persistence)

Next Steps

  • Shared Actors — add a second function that reads/writes the same Account through a different binding
  • Runtime APIStatefulActor, ctx, storage, alarms
  • Alarms — schedule deferred work from inside an actor method

How It Works

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/concepts/how-it-works.md
Take a concrete problem: keep a balance per account, reachable over HTTP, correct when requests overlap, and intact across restarts. A debit must never overdraw, and a committed debit must never be lost. Below is what that actually requires, from first principles, and how a stateful actor hands you the same thing.

How You’d Build This, from First Principles

Strip it to the metal. One process serves every account — a, b, c, x, y, … — holding them all in an in-memory map and handling a request for whichever one comes in. Two things make each debit correct: a per-account lock, so two debits on the same account can’t interleave (while debits on different accounts still run in parallel), and a write-ahead log + fsync, so a committed debit is on disk before you reply.
Durability is append-to-log then fsync — you don’t acknowledge the debit until the bytes are on disk:
A request names an account; the process looks it up and serves it. Lock that account, check-and-decrement, log, fsync, unlock:
Two primitives do all the work: the lock is the serialization point, and fsync before you reply is durability. Everything else follows:
  • one process owns it all — the map, the per-account locks, and the log are shared by every account ay;
  • durability is the log: a debit is committed once its record is fsync’d, so a crash loses only what was never acked;
  • recovery is not replaying an ever-growing log from zero — that gets slower forever. You periodically write a snapshot of all balances and keep only the log records since it; on restart you load the snapshot and replay that short tail. (Postgres’ checkpoint + WAL, SQLite’s WAL mode, and Redis’ RDB + AOF all do exactly this.) The on-disk snapshot + log is the truth; memory is a cache;
  • to grow past one process you shard accounts across processes and machines, routing each account to the one that owns it — and this is where most people stop hand-rolling and reach for a database, which is exactly this machine (sharded, replicated, snapshot + write-ahead log) run by someone else. Postgres’ COMMIT is what persist_data does, its checkpoint is your snapshot, and SELECT … FOR UPDATE is the lock.
You can build all of this. The work isn’t any single line — it’s that you own the lock discipline on every path, the log format, the snapshots, the recovery, the sharding, and the restarts, forever.

The Same Thing as a Stateful Actor

The class is only the logic half. In the C version, debit(acct_id, …) did the lookup itself (lookup_or_create) and the logic. With an actor that lookup moves to the caller: a normal function parses the request, names the account with idFromName, and calls the method — which now takes only amount and never has to find its own state.
idFromName(acct) is the actor-world lookup_or_create — instead of finding a struct in this process’s map, it routes to the one instance that owns that account, anywhere (the “Reaching one account” row below). Same check-and-decrement, same durability requirement — but no pthread_mutex, no fsync, no log, no snapshots, no shard map. The two primitives are still there; the runtime provides them. Here’s the mapping, mechanism by mechanism: The runtime takes the one process that multiplexed a, b, c, x, y and splits it: each account id becomes its own instance. idFromName("a") and idFromName("b") are two different instances, on two different threads, each with its own storage — debits on a serialize, debits on a and b run in parallel, and there is no shared map, per-account lock, or single log to own. A stateful actor is that machine — a single-threaded server with durable, flush-before-ack storage, one per account — that the platform shards, routes, and restarts for you. You write the body of debit; the runtime is the lock, the durability, the recovery, and the router. The four guarantees the runtime makes from this mapping are stated precisely in Execution model.

Next Steps


Execution Model

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/concepts/execution-model.md
The runtime makes four guarantees. Each is the managed form of a mechanism you’d otherwise build by hand — the C server and mapping table in How it works show where each one comes from. Stated precisely: 1. Exactly one instance per name. idFromName("acct_123") routes every call for that name to one owner — the shard map and router you’d otherwise build by hand, done for you. No two hosts run account acct_123 at once, so the instance is the source of truth for that account. 2. One call at a time — no concurrency inside an instance. This is the per-account pthread_mutex, except you don’t write it and can’t forget it. One thread per instance runs each method to completion before the next starts. await yields the thread, but the next call still won’t start until your method returns. The C debit is correct because it holds the lock across the change; the actor debit is correct because two debits on one account never run at the same time. 3. Writes are durable before the caller sees a result. This is write() + fsync() before you reply. When your method returns, the runtime holds the response until every write you issued has been flushed — the caller can’t observe a result built on an unflushed write. (If a write rejects after the method returns, the call fails with ActorOutputGateError instead of returning success.) 4. Only persisted state survives; memory is a cache. This is “the on-disk snapshot + log is the truth; the in-memory map is a cache.” An instance can be evicted or restarted between any two calls — a deploy, idle eviction, host failure — and instance fields (this.x) vanish while ctx.storage does not. The runtime does the snapshotting and recovery; you just read from storage. Treat memory exactly like a process that can be SIGKILLed at any instant: only what you flushed to storage is real.

Next Steps


Lifecycle & Placement

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/concepts/lifecycle.md
A stateful actor instance is activated on demand, kept in memory while it’s busy, evicted when idle, and can be restarted or relocated at any time. You don’t manage any of this — but you design around it, because only what you write to storage survives.

Where it Runs

An instance is placed on first touch — the first method call to a name brings it up. After that you address it by name, never by location; the platform routes every call to wherever the instance currently lives. (idFromName accepts a locationHint option, reserved for regional placement — see Actor Namespace.)

Activation and Eviction

  • Activated on demand. Naming an instance is cheap; the first method call to a name brings the instance up and routes the call to it.
  • Kept warm while busy, evicted when idle. Between calls, an idle instance may be removed from memory to free resources, then re-activated on the next call.
  • Restarted on deploy or host failure. A new code version or a host issue tears the instance down and re-creates it.

What Survives a Restart

In-memory state — instance fields like this.x — is a cache. It vanishes on eviction, restart, or relocation. Your actor’s storage is the truth: it is durable and reloaded when the instance next activates. Treat memory exactly like a process that can be SIGKILLed at any instant — only what you flushed to storage is real. (This is guarantee 4 in Execution model.) A common pattern is to load from storage once on activation and cache the result in memory; just never assume the cache is still there.

Next Steps


Addressing

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/concepts/addressing.md
env. is your handle to an actor class. You name an instance, get a stub, and call methods on it.

Two Ways to Name an Instance

  • idFromName(name) is deterministic: the same name always lands on the same instance, from anywhere. This is the common case — pick a stable identity for the entity and let the platform route to its state.
  • newUniqueId() mints a fresh, unguessable instance; you store the id (stub.id) yourself. It currently fails at call time on the platform (a known runtime issue — see Actor Namespace); until it’s fixed, mint an identity yourself and pass it to idFromName.

Choosing a Name

The name is your shard key — choose the entity’s natural identity: a caller’s E.164 number, a user or account id, a room id, or a composite key (tenant:room). Same name → same single-threaded instance → coherent state for that entity. Spreading load means choosing names that spread across entities; a single hot name is one single-threaded bottleneck (see When to use).

Calling the Stub

Both calls return a stub. Calling a method on the stub is an RPC to that one instance: the call is queued there, your method runs, the value comes back.

Next Steps


Overview

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/api-reference.md
The types in this reference are exported from @telnyx/edge-runtime (TypeScript). The Stateful Actor surface at a glance — follow a link for the full page:
  • Bindings — how bindings resolve on env
  • KV — globally distributed key-value storage
  • Execution Model — function lifecycle and concurrency

Base Class

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/api-reference/base.md
StatefulActor is the base class you extend. Subclass it, declare async methods, and they become RPC-callable from a stub. It holds this.ctx (see Actor Context) and this.env (your bindings).

Construction

You don’t construct actors yourself — the runtime does. The base constructor wires this.ctx and this.env, so a subclass that doesn’t need init logic can omit its constructor entirely.
If you do need one-shot init (preload state, set up an initial alarm), override the constructor and call ctx.blockConcurrencyWhile — see Actor Context.

Dispatch Rules

  • Public methods are RPC-callable from a stub — declare them async (every stub call is a Promise on the caller’s side regardless).
  • Methods whose names start with _ are internal helpers and are not RPC-exposed — the runtime rejects the call.
  • The _ prefix is the only opt-out. TypeScript’s private keyword is erased at runtime and does not stop remote calls; name internal helpers with a leading _.
  • fetch(req), alarm(info), webSocket(ws, req), the constructor, and methods defined on StatefulActor itself (not your subclass) are special and not auto-RPC.
  • Methods have a wall-clock budget (30s by default). A call that exceeds it fails with ActorMethodTimeoutError — see Errors.

alarm(alarmInfo)

Optional alarm handler. Override to handle scheduled work. The base implementation is a no-op.
See Alarms for the delivery contract and retry policy.

fetch(req)

Optional HTTP-style entry. The base default returns 404. Override if you want callers to reach the actor over a raw Request instead of RPC methods:
Callable from a binding as env.BINDING.idFromName(name).fetch(req).

webSocket(ws, req)

Optional WebSocket entry. Unlike alarm() and fetch(), there is no default implementation — declaring the method is the opt-in. An actor that doesn’t declare webSocket() has no socket behavior at all, and an upgrade routed at it is closed with code 1011 (reason actor has no webSocket handler).
  • Called once per accepted connection, with the live socket and the handshake Request. req is the request your function’s front door forwarded: the URL plus application headers, with transport and handshake headers stripped — see WebSockets for the contract. Client-sent app headers pass through too, so trust only headers your front door explicitly set or overwrote — the front door must overwrite or strip anything auth-bearing (like x-user above) before forwarding.
  • ws is a Node ws socket (import type { WebSocket } from "ws"): ws.on("message", (data, isBinary) => ...), ws.send(data), ws.close(code, reason). There is no WebSocketPair/accept() shape.
  • Register your listeners before webSocket() returns. Frames that arrive before webSocket() returns — including any the client sent immediately after the handshake — are buffered and replayed in order once it returns; nothing drops.
  • Socket events share the instance’s single-threaded dispatch. message, close, and error handlers run one at a time, serialized with RPC methods and alarms, each under the method wall-clock budget (30s by default). A message handler that throws or exceeds the budget closes the socket with code 1011.
Connections reach the actor through your function’s fetch front door: check the Upgrade header, authenticate, then forward with env.BINDING.idFromName(name).fetch(...). See WebSockets for the front door, connection lifetime, close codes, and delivery semantics.

Actor Context

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/api-reference/context.md
this.ctx (an ActorContext) holds the instance’s identity, per-key storage, single alarm, one-shot init, and the instance’s live WebSocket connections.

ctx.id

The customer-supplied name for this actor — opaque string. You chose it via env..idFromName(name). Common patterns: caller E.164, CRM user id, email, or a composite key.

ctx.blockConcurrencyWhile(fn)

One-shot init primitive. Use it inside a subclass constructor to gate all other calls until init finishes. 30s budget — a callback that exceeds it fails init (BlockConcurrencyTimeoutError), and the activation is torn down and retried on the next call.

ctx.setAlarm(when)

Top-level alias for ctx.storage.setAlarm(when). when is ms since epoch. See Alarms.

ctx.count()

The number of WebSocket connections open on this instance right now. Synchronous — no await. Scoped to the instance: sockets on other names of the same actor class are never counted, and a socket never carries over between names. Callable from any handler — a socket message handler, an RPC method, or the alarm handler.

ctx.broadcast(data)

Send one frame to every WebSocket open on this instance; returns the number of sockets the frame was sent to. A string is sent as a text frame; an ArrayBuffer or ArrayBufferView as a binary frame. Scoped to the instance, like count() — a broadcast never reaches another name’s sockets. Like ws.send(), the write happens immediately; it is not held until the turn’s storage writes commit. Callable from any handler. Calling it from alarm() is the server-initiated push pattern — the actor sends with no inbound frame prompting it:
See WebSockets for how sockets reach the actor in the first place.

Actor Storage

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/api-reference/storage.md
this.ctx.storage (an ActorStorage) is the actor’s per-key persistent storage. A turn that returns successfully is persisted — all writes made during the turn (put/delete/transaction/sql.exec/setAlarm) commit atomically at end-of-turn; a turn that throws commits nothing.
Key behaviors:
  • Lazy per get — state is not preloaded on activation. Call get when you need it.
  • Read-your-writes — reads always reflect prior writes from the same actor.
  • Per-actor method serialization — calls to one actor instance are dispatched one at a time, giving you effective ACID at the actor level.
  • Values round-trip through a codec. JSON natives plus Date, Map, Set, typed arrays, ArrayBuffer, BigInt, and RegExp come back as what you stored. Unstorable values — functions, class instances, circular structures, promises, streams — are rejected with a CodecError at the write.
  • Keys starting with __telnyx_ are reserved for the runtime’s own bookkeeping — writes to them are rejected.

list(options)

Lexicographic key order; reverse: true flips it. Returns a Map to preserve ordering.

transaction(fn)

Atomic batch. fn receives a StorageTransaction with the same get/put/delete/list surface.
If fn throws, the buffered writes are discarded — and the enclosing method call fails with ActorOutputGateError, even if your code catches the rejection. Only the transaction’s own writes are undone: writes you make after catching the rejection still commit, but the caller sees the error instead of your return value. A thrown transaction is not a control-flow tool for “try the write, continue on failure”.

sql

ctx.storage.sql is a private embedded SQLite database for this actor — a separate, synchronous store beside the key/value surface above. See SQL storage for the exec / cursor contract, types, transactions, limits, and durability.

transactionSync(fn)

Atomic SQL transaction: runs fn synchronously, commits its sql.exec writes when it returns (passing the return value through), rolls all of them back if it throws. fn must not be async — an async callback throws SqlAsyncTransactionError and commits nothing. This is the only transaction API for SQL: exec() rejects raw BEGIN / COMMIT / ROLLBACK / SAVEPOINT with SqlTransactionControlError. Unlike a thrown transaction(fn), a thrown transactionSync does not poison the turn: catch it and the method call completes normally.

Actor Namespace

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/api-reference/namespace.md
env. is an ActorNamespace — the handle on env that your function calls into. It’s declared in telnyx.toml:

idFromName(name, options?)

Deterministic — the same name always lands on the same actor instance. This is the most common call: pick a stable identity (caller E.164, CRM id, room id) and let the platform route to the right state. locationHint is a placement hint, relevant on first touch only — an actor materializes once, so a hint on a later call can’t move it. The platform may ignore the hint entirely (today it does — the option is accepted and reserved for regional placement). Abstract region codes (the taxonomy can evolve without breaking your code).

newUniqueId(options?)

Random — a fresh, unguessable instance. Use when you want the runtime to mint the identity for you (e.g. a one-shot job actor). The id is generated client-side; keep it (via stub.id) if you’ll need to reach the instance again. newUniqueId currently fails at call time on the platform (a known runtime issue). Until it’s fixed, mint an identity yourself and pass it to idFromName. Both return an Actor Stub. Method dispatch on the stub is provided by the runtime via a Proxy; in TypeScript, run telnyx-edge types to type env. against your class’s public method shape, or hand-roll the narrowing — the Actor Stub page shows both.

Actor Stub

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/api-reference/stub.md
An ActorStub is the client handle returned by idFromName / newUniqueId. Calling a method on it is an RPC to that one actor instance.
telnyx-edge types generates the narrowing for every [[actors]] binding in telnyx.toml — a telnyx-env.d.ts that types env. against your class’s public method shape. To hand-roll it instead (also the path for a shared-actor reference, which ships no local class for codegen to read):

Actor Alarms

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/alarms.md
Every Stateful Actor has a single per-instance alarm — a one-shot timer you can set, replace, read, or clear from inside any method. When the alarm fires, the runtime calls the actor’s alarm(alarmInfo) handler. Alarms are the way to do deferred work without an external scheduler. There is one alarm per actor instance, not per method or per key. Setting it again replaces the previous time.

Set and Handle

The handler is just another method call on the actor — it inherits the same single-threaded dispatch guarantee as any other method. You don’t need a lock around the state the alarm touches.

Delivery Contract

AlarmInfo:
  • At-least-once delivery. Plan for the handler to run more than once for the same scheduled time. Make writes idempotent — re-check state in storage before applying effects (as the Session example above does), or record a done-marker keyed by the scheduled time.
  • A failed run is redelivered 3 times, about a second apart. If the handler throws (or exceeds its time budget), the platform re-fires it with retryCount incremented — 4 attempts total, then the alarm is deleted (getAlarm() returns null). There is no signal when that happens.
  • A failing handler loses its alarm — don’t use throw as your retry mechanism. Three retries a second apart won’t outlast a real outage. If the deferred work must happen, make the handler tolerate its own errors: catch, record, and re-arm with setAlarm at a backoff you choose.
  • Use info.isRetry to branch. On retry, log it, treat the work as best-effort, or skip already-completed steps.

Patterns

TTL / expiry

Set the alarm when you create the row; on fire, check the row is still expired and delete it. If the row was touched since, reset the alarm instead.

Retry backoff

If your method kicks off a flaky external call, set an alarm to retry later; on fire, attempt again and either succeed or set a new alarm with a longer backoff.

Session timeout

See Session.touch above — every interaction pushes the alarm out by the idle window; the alarm only fires if no one touches the session in time.

Coalesced drain

Because there’s only one alarm per instance, you can use it as a “flush trigger”: set it on the first write, and when it fires, drain the queue and clear it. Subsequent writes during the window just append to the queue.

Limits

  • One alarm per actor instance. If you need multiple timers, keep a queue in storage and use the single alarm to drive a scheduler loop.
  • when is ms since epoch. Don’t pass seconds. Firing precision is about one second — don’t schedule sub-second work with it.
  • No recurring flag. If you want periodic work, set the next alarm at the end of your alarm handler.
  • The handler has a time budget. alarm() runs under its own wall-clock cap — larger than the 30-second method budget, on the order of minutes, but still bounded. A run that exceeds it counts as a failure and is redelivered.
  • deleteAll() does not clear the alarm. Keys and the alarm are separate; a pending alarm still fires after deleteAll(), and a handler that re-arms itself will keep running against empty state. To decommission an actor: deleteAlarm() first, then deleteAll().

Next Steps


Configuration

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/api-reference/configuration.md
A Stateful Actor project uses the umbrella telnyx.toml manifest (the same file your [[secrets]] and [telnyx] bindings live in). The actor declaration is a [[actors]] array:
Constraints, enforced at ship time: binding and type must be identifiers and must not contain __; type is capped at 32 characters; binding names must be unique across all bindings in the file, and SECRETS is reserved as a binding name when the file has a [[secrets]] block. Multiple [[actors]] blocks are allowed — including two bindings pointing at the same type.

Errors

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/api-reference/errors.md
On the caller side, a failed stub call rejects with an Error whose name identifies the cause — for example ActorMethodError (your method threw; message preserved), ActorPrivateMethodError (_-prefixed method), or ActorUnknownMethodError. Branch on err.name if you need to distinguish them.

Project Structure

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/guides/project-structure.md
A Stateful Actor project ships from one module that exports two things, wired together by a binding declared in telnyx.toml.

Two Exports

One module exports the actor class and a fetch handler — an ordinary Edge Compute function that calls the actor through a binding on env. The deploy pipeline routes each export to the right runtime.

The Binding

env.ACCOUNT is declared in telnyx.toml as an [[actors]] entry — binding is the property on env, type is the class to instantiate per name:
See the Runtime API for the full telnyx.toml actor block. The Quick Start builds and deploys exactly this shape — the Account actor plus its fetch handler — end to end.

Next Steps


Local Development

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/local-development.md
A plain Edge Compute function is an ordinary program you run and curl directly — the Functions local-dev guide covers that. A Stateful Actor project is different: the fetch handler reaches its actors through an env. call, and that binding resolves only against a running actor runtime. There is no in-process emulation to fall back on. telnyx-edge dev supplies the missing runtime. It generates the actor stack your project needs, boots it in Docker, serves your fetch handler on a local port, and hot-reloads on save — so env. calls hit real actors before you ship. telnyx-edge dev is a development command; it does not deploy anything. When it works locally, ship with telnyx-edge ship.

Prerequisites

  • Docker, running. The stack is a set of containers booted with docker compose.
  • A telnyx.toml umbrella project — the two-export shape from Project structure: one module that exports an actor class and a fetch handler, wired by an [[actors]] binding. Run telnyx-edge dev from the directory that holds the manifest, or point at it with -f/--from-dir. Without a telnyx.toml, the command errors:
    The Quick Start scaffolds exactly this shape with telnyx-edge new-func --actor.

The Loop

From an umbrella project directory:
This bundles the project, generates the stack under .telnyx/dev, and boots it. Your fetch handler is served on http://localhost:8787 (change it with --port). Exercise it the same way the Quick Start exercises the deployed URL — but against localhost, and with actors resolving locally:
Each env.ACCOUNT.idFromName(...) call routes to a real actor instance in the local stack — the same code path that runs deployed. Edit your actor class or fetch handler and save; telnyx-edge dev watches the source and hot-reloads the affected runtime. Actor state lives in the stack’s Postgres store, so it survives a reloadalice is still 70 after you edit and save. Press Ctrl-C to stop watching. The stack keeps running; the command only detaches the file watcher.

What the Stack Contains

telnyx-edge dev writes the stack under .telnyx/dev and runs it with docker compose. It is a local stand-in for the deployed platform, containing:
  • the function runtime — serves your fetch handler on the published port;
  • the actor runtime — hosts your actor instances;
  • a Dapr sidecar for each runtime, plus Dapr placement — the routing layer that sends each actor name to its owner;
  • a Postgres actor state store — where ctx.storage persists, so state survives reloads.
env. calls from the function runtime reach the actors in the actor runtime through this layer. This is what a plain func.toml function lacks: it has no local binding emulation, so binding-backed code paths only run once deployed.

Flags

Use --generate-only to inspect or customize the generated compose stack before booting it, and --no-watch when you want the stack up for an out-of-band test run (for example, from a CI script) rather than an interactive edit loop.

Notes

  • Actor state survives reloads. Because ctx.storage persists to the stack’s Postgres store, a hot reload does not reset your instances — balances, counters, and any other state carry across saves. Tear the stack down with docker compose (or remove .telnyx/dev) when you want a clean slate.
  • Ctrl-C leaves the stack running. It stops the watcher, not the containers. Stop the stack explicitly with docker compose when you’re done.
  • A telnyx.toml is required. telnyx-edge dev runs umbrella projects. A single-function func.toml project has no actors to host — develop those with the Functions local-dev workflow instead.

Next Steps

  • Quick Start — scaffold, write, and deploy an Account actor end to end
  • Project structure — the two-export shape telnyx-edge dev runs
  • Deploy — ship, revisions, and rollback

Shared Actors

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/shared-actors.md
A shared actor is a single Stateful Actor type reachable from two or more Edge Compute functions on the same account. The function that ships the actor class owns the actor; other functions on the same account declare the same type under their own binding name and ship no class — their binding routes to the owner’s actor and its state.

Why Share an Actor

  • Split read and write paths into separate functions with their own auth, rate limits, and deploy cadence — without duplicating state.
  • Let a second function call into a domain the first one owns (e.g., a reporting function reads an account actor that a payments function owns).
  • Keep one durable actor behind many entrypoints. Both functions talk to the same per-name state; writes through one binding are visible to reads through the other.
Both functions must be on the same account — shared actors are bounded by account.

The Pattern

The owner function is a normal Stateful Actor project — it ships the class. The reference function declares the same type under a different binding and ships no actor class — its binding routes to the owner’s actor and state; no second instance is created. There’s no new-func flag for “reference-only,” so the workflow is: scaffold with --actor (to mint a func_id and write telnyx.toml), then drop the actor class and rename the binding.

Walkthrough: a Read-Only Account Reference

Prereq: an owner function for the type already shipped on your account (the account from the Quick Start, type Account).

1. Mint a func_id and scaffold, then drop the class

2. Point telnyx.toml at the owner’s type under a new binding

Edit the [[actors]] block — keep type equal to the owner’s type, change binding:
Leave name, main, compatibility_date, and the [edge_compute] func_id as scaffolded.

3. Replace src/index.ts with a class-free handler that calls env.READONLY

Key point: no export class Account in this file. The bundler roots at main, so with nothing importing the class, the reference bundle ships class-free (a few KB) — that’s what makes it reference-only.

4. Install deps and ship (same as the owner)

5. Prove they share one actor

Write through ACCOUNT, read through READONLY, same balance → one durable actor (Account), two functions. Both must be on the same account; the reference’s type must exactly match the owner’s.

Rules of Thumb

  • Same account, same type. Reference bindings whose type doesn’t match an owner already on the account won’t resolve.
  • Only one function owns the class. The owner ships the actor class; every other function declares the same type and ships no class.
  • Renaming the binding is fine. Each function picks its own binding name — that’s the property on env for that function. The type is what glues them together.
  • Reference bundles are small. With no class import, the bundler emits a class-free bundle. Keep it that way: don’t import the actor class from your reference function.

Next Steps

  • Quick Start — ship the owner function this reference calls into
  • Runtime APIActorNamespace, ActorStub, and the binding surface
  • Alarms — schedule deferred work from inside an actor method

When to Use Stateful Actors

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/guides/when-to-use.md
Use a stateful actor when an entity has state that must be mutated coherently across many requests — a cart, a chat room, a call leg’s media state, a per-user rate limiter, a job coordinator. One instance per name gives you serialized, durable, private state for that entity without locks or a shared database.

When Not to Reach for One

  • Stateless work (routing, transforms, independent fan-out): use a plain function. An actor only adds a routing hop and a serialization point you don’t need.
  • A global counter or singleton: one name is one single-threaded instance is one throughput ceiling. Route everything through idFromName("global") and you’ve rebuilt the single-lock bottleneck. Shard across many names so load spreads across instances.
  • Large blobs or bulk scans: the keyspace holds one entity’s working set, not a warehouse. Use object storage or a database.
  • Cross-entity transactions or ad-hoc queries: each instance is its own consistency domain. Within one actor, writes are serialized and transaction() gives you atomic multi-key updates — but there is no transaction across two actors, and there’s no JOIN across them. If you need a transaction spanning many entities, or to query across them, that’s what a database is for — keep it.
A single instance processes calls serially, so its ceiling is roughly 1 / (method wall-time) calls per second — the same ceiling one thread per account gives you. Sharding by a well-chosen name is how you scale.

Next Steps


Key-Value

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/guides/storage/key-value.md
Every Stateful Actor has private, durable key/value storage at this.ctx.storage. It is the default place to keep an entity’s state: a cart’s items, a call leg’s status, a per-user counter. Reach for it first, and drop to SQL only when a lookup by key isn’t enough and you’d otherwise scan. The store is private, strongly consistent, and transactional: only this actor instance reads and writes its keys, its methods run one at a time (serialized turns), and a write that returns successfully is durable — it survives the actor being evicted, relocated, or its pod replaced. Reads always reflect your prior writes. This is a guide. For the exact method signatures, list options, and codec rules, see the Actor Storage reference.

Reading and Writing

get and put are the whole surface for simple state. Both are async — they may cross the network — so await them:
State is lazy per get — nothing is preloaded when the actor activates; you read a key when you need it. Values round-trip through a codec, so JSON natives plus Date, Map, Set, typed arrays, ArrayBuffer, BigInt, and RegExp come back as what you stored. Functions, class instances, promises, and circular structures are rejected at the write. Keys are strings. Those starting with __telnyx_ are reserved for the runtime — writes to them are rejected.

Listing Keys

list returns a Map in lexicographic key order, which makes prefixes a natural grouping:
limit defaults to 128 and maxes at 1000. Use start / startAfter / end to page through a large keyspace. See the reference for the full ListOptions.

Atomic Updates

transaction batches multiple keys into one atomic unit — all of the writes land, or none do:
If the callback throws, its buffered writes are discarded. Note that a thrown transaction also fails the enclosing method call — it is not a “try the write, continue on failure” tool. See the reference for the exact semantics.

Clearing an Actor

deleteAll() atomically removes every key for this actor. It does not clear a pending alarm — delete that separately if you want the actor fully reset.

Key/Value or SQL?

Both live under this.ctx.storage, are durable and strongly consistent, and can be used together in the same actor.
  • Key/value (this page): simple keyed state, prefix listing, atomic multi-key updates. The default.
  • SQL: relational shape, aggregates, GROUP BY, secondary indexes, and joins within one actor — when a keyed lookup isn’t enough.

SQL

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/guides/storage/sql.md
Every Stateful Actor has a private, durable embedded SQLite database reachable at this.ctx.storage.sql. It sits beside the key/value store under the same this.ctx.storage, but it is a separate engine: one SQLite database per actor instance, queried with real SQL. Reach for it when a single entity’s state is relational or needs queries the key/value store can’t answer without a scan — aggregates, GROUP BY, secondary indexes, joins within the one actor. It is not a shared database: each instance sees only its own data, and there are no cross-actor queries or joins. The database is private, strongly consistent, and transactional: one actor instance is the only reader and writer of its database, its methods run one at a time (serialized turns), and a write that returns successfully is durable at turn granularity — it survives the actor being evicted, relocated, or its pod replaced. Reads always reflect your prior writes. Stateful Actors are in Beta. ctx.storage.sql requires @telnyx/edge-runtime 0.6.0 or later; the transactionSync() type ships in 0.8.0, so use the latest SDK.

The Surface

exec is synchronous — it does not return a Promise. This is deliberate and unlike every other member of ctx.storage: the database is a local file in the actor’s own process, so there is no network hop to await.
There is no schema step in the manifest — any actor that calls ctx.storage.sql gets a database, created on first use. Create your tables with CREATE TABLE IF NOT EXISTS (a cheap no-op once they exist) at the top of the methods that need them.

Binding Parameters

Use positional ? placeholders and pass the values as trailing arguments. Always bind user input — never build SQL by string concatenation. (Binding here is SQL’s own term for attaching values to placeholders — no relation to the telnyx.toml bindings your function receives on env.)
A bound value must be one of four types: null, number, string, or ArrayBuffer (for binary). Result columns come back as those same four types:

Reading Results: the Cursor

exec returns a SqlCursor — a single-pass iterator over the result rows. Drain it once, either by iterating or with toArray():
Single-pass means the rows are consumed as you read them. After you iterate a cursor to the end, toArray() on that same cursor returns []; calling toArray() a second time also returns []. If you iterate partway and then call toArray(), you get the rest of the rows. Run exec again for a fresh pass. exec is generic in the row type — exec(...) types the returned rows — but the type is a compile-time assertion only, not validated at runtime, so make sure your query returns those columns.

Multiple Statements in One exec

A query string may contain several ;-separated statements — useful for pasting a schema or a small migration. Every statement executes, in order; the bound parameters apply to the last statement, and the returned cursor is over the last statement’s rows:
Statements run independently — if one fails, exec throws, but the statements before it have already been applied. Wrap the batch in a transactionSync callback when it must be all-or-nothing. Multi-statement splitting is a best-effort TypeScript tokenizer, not exact SQLite parsing. It handles all realistic SQL, but unquoted keyword-identifiers (a column named end, begin, or case in a deep position) can confuse the splitter. Quote reserved-word identifiers ("end") to avoid edge cases. Exact parity is tracked in COMPUTE-470.

Transactions

Wrap related writes so they commit together — or roll back together — with transactionSync. It runs your callback synchronously, commits when it returns, and rolls back every write if it throws:
The callback returns a value — transactionSync passes it through, so you can read back a result computed inside the transaction. Two rules, both enforced by the runtime:
  • transactionSync is the only transaction API. Raw transaction-control statements — BEGIN, COMMIT, ROLLBACK, SAVEPOINT — are rejected by exec() with SqlTransactionControlError, anywhere in a statement batch. (An open transaction left dangling across method calls would have no owner; the callback shape makes that impossible.)
  • The callback must be synchronous. Passing an async function throws SqlAsyncTransactionError and commits nothing — the transaction commits when the callback returns, so statements after an await would run outside it. Do async work before or after, not inside.
Because a single actor instance processes one method at a time, you never contend with another writer inside your own database — the actor is the lock.

Bulk Writes

Each standalone exec write commits and flushes to disk on its own, which is slow in a loop. Wrap a batch in one transactionSync and it commits (and flushes) once:
The difference is large: thousands of individual auto-commit inserts take seconds; the same inserts inside one transaction take tens of milliseconds. Batch anything hot.

Indexes

The database holds one entity’s working set, but scans still cost time and — as it grows — memory. Add an index on any column you filter or order by frequently:

Limits

  • 1 GB per actor database. Enforced by the engine — writes past it fail.
  • 2 MiB per bound value. A bind larger than 2 MiB (2,097,152 bytes) is rejected with SqlParameterTooLargeError before the statement runs. There is no cap on output row size.
  • Up to 32,766 bound parameters in a single statement (SQLite’s variable limit).
  • Integers come back as JavaScript number. SQLite stores 64-bit integers, but exec returns integer columns as JS numbers — the same design as Cloudflare’s SQLite storage and D1, neither of which returns BigInt from the query API. A value beyond Number.MAX_SAFE_INTEGER (2^53 − 1, i.e. 9007199254740991) can be written and compared in SQL, but reading that column throws RangeError: Value is too large to be represented as a JavaScript number — a loud failure rather than a silently less-precise number. Store large ids, bitmasks, or nanosecond timestamps as TEXT (or BLOB), or keep integers within the safe range.
  • Foreign keys are ON — a REFERENCES violation raises FOREIGN KEY constraint failed.
  • Failed statements (syntax errors, constraint violations) throw from exec; the actor keeps running, so catch and handle them like any other error.

SQL or Key/Value?

Both live under this.ctx.storage, are durable and strongly consistent, and can be used together in the same actor.
  • Key/value (get/put/list): simple keyed state, prefix listing, atomic multi-key updates — reach for it by default when a lookup by key is all you need.
  • SQL (ctx.storage.sql): relational shape, aggregates, GROUP BY, secondary indexes, and joins within one actor. Reach for it when a keyed lookup isn’t enough and you’d otherwise scan.

WebSockets

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/websockets.md
WebSocket support for Stateful Actors is in beta. APIs and limits may change before general availability. A client opens one WebSocket. Your function runs once, at the handshake — check the upgrade, authenticate, decide which actor owns this connection — and hands off. From then on the actor owns the live socket: each inbound frame is dispatched like a method call — one at a time per instance, serialized with RPCs and alarms — durable state is ordinary ctx.storage, and replies stream straight back with ws.send(). That shape removes the plumbing a chat, agent, or live-session backend usually accretes. When the connection tier and the state tier are separate fleets, every message crosses a pub/sub bridge on the way in and needs a reverse channel to find whichever host holds the socket on the way out. Here the socket and the state live in the same single-threaded instance — a message handler is a plain method body that reads storage, writes storage, and sends. Any actor class can terminate WebSockets — declaring the webSocket() method is the whole opt-in; there is no flag to enable, in telnyx.toml or anywhere else. Actor names used with sockets must be printable ASCII with no leading or trailing whitespace, and any header values your function forwards must be printable ASCII today.

The Two Halves

The boundary is strict: per-message logic never lives in the function. After the handshake the function is done — frames are delivered only to the actor. Everything the function learned rides the forwarded request as headers, so the socket reaches the actor already authenticated. The req the actor sees carries only those application headers; transport and handshake headers (Upgrade, Sec-WebSocket-*) are stripped. The smallest complete pairing:
(authenticate and roomFor stand in for your auth and routing; a complete, runnable version is below.) The actor opts in by defining one optional method:
ws has Node ws socket semantics — ws.on("message", (data, isBinary) => ...), ws.send(data), ws.close(code, reason) — not a browser socket and not Cloudflare’s WebSocketPair/accept(). Text and binary frames both work. An actor with no webSocket() method closes incoming sockets with code 1011.

A Chat Room, End to End

One module exports both halves. The ChatRoom actor holds the room’s messages and its live sockets; the default export is the front door.
Connect with any WebSocket client:
Every socket opened on /rooms/lobby lands on the same ChatRoom instance; /rooms/standup is a different instance with its own seq, its own messages, and its own sockets. ctx.count() and ctx.broadcast() see only this instance’s sockets — a broadcast never crosses actor ids, and a socket never moves between them.

Sending to Some Sockets, Not All

ctx.broadcast() is all-or-nothing: one frame to every socket on the instance. There is no built-in way to address a subset — no socket list to filter, no per-socket tags. “Everyone except the sender” and “just this user” are yours to build, and on a single-threaded instance the whole mechanism is one Map: filled in webSocket(), pruned in the close listener.
The map is a plain instance field, and here — unlike almost everywhere else on the platform — that is correct, with nothing written to ctx.storage. Sockets and instance memory share a lifetime: an open socket keeps the instance resident, and the events that replace an instance — a redeploy, an infrastructure restart — sever its sockets too. A fresh instance therefore starts with no sockets and an empty map, never one without the other; there is nothing to persist and nothing to rebuild. What should outlive the connection — who was here, what they said — goes in ctx.storage as usual.

Messages Are Single-Threaded, Like Everything Else

An inbound frame is dispatched like a method call, under the execution model’s “one call at a time” guarantee: while a message handler runs, frames from other sockets on the same instance, RPC calls, and the alarm all wait. That is exactly what makes the chat room’s read-modify-write on seq safe with any number of concurrent senders. The consequences:
  • The 30-second method budget applies to each message handler. A handler that throws or exceeds it closes the socket with code 1011.
  • ws.send() is immediate — and not held for durability. A method’s return value is held until your storage writes are durable; a sent frame is not. If a handler sends and then fails, the client keeps a frame describing state that never committed. If clients must act only on committed state, make frames re-derivable from storage on reconnect — the chat room’s durable seq makes gaps and replays detectable.
  • Keep per-message work bounded. Frames that arrive while a handler runs queue, up to 256 events or 1 MiB; overflow closes the socket with 1013 (back off before reconnecting), and a single inbound frame over 1 MiB closes it with 1009. Don’t stream minutes of work from one handler — chunk it across messages, or drive it from an alarm. ws.send() itself never blocks the handler.
  • High-frequency senders should coalesce. Every frame costs a full dispatch — one turn of the instance’s single thread, one queue slot — so a client emitting a stream of tiny messages (sensor readings, cursor positions, game state) spends the queue on frame count, not bytes: at 100 messages a second, a single handler that stalls for three seconds is enough to overflow the 256-event queue and close the socket with 1013. Batch logical messages into one frame — an array, flushed on a short timer or a count threshold — and loop over the batch in the handler. Fifty readings in one frame cost one dispatch and one queue slot instead of fifty; just keep the batch under the 1 MiB frame cap.
An open socket also keeps the actor in memory for the duration of the connection.

Connections End; State Doesn’t

Every connection ends. Today a connection lives at most about five minutes from the handshake — even on a socket actively exchanging frames — and the cutoff surfaces as an abnormal close (code 1006, no close frame). This cap will be raised in a future update. Function redeploys and infrastructure restarts sever sockets the same way; a deliberate close from either side is code 1000. Reconnecting is the client’s job: back off, reopen, rejoin the same name. idFromName routes the new socket back to the same actor, and everything in ctx.storage survived — the room’s messages and seq are intact; only the socket is new. A reconnect is a new socket on the actor, never a resumption of the old one. The full close-code table and reconnect guidance are on Connection Lifecycle.

Push Without an Inbound Frame

An actor runs only when something calls it — a frame, an RPC, or an alarm; time passing alone runs nothing. To push to connected clients without waiting for them to send anything, let the alarm make the call and fan out with ctx.broadcast():
The alarm handler is an ordinary method call, so it can read storage, write storage, and send — a message handler on one socket can likewise broadcast to all of them, as the chat room does.

Outbound Requests

Egress from an actor is ordinary Node: the global fetch and the global WebSocket client both work from inside any handler — call a model API and stream its output back over the socket, or dial an upstream WebSocket, exchange frames, and close it. The dispatch rules apply unchanged: outbound work inside a message handler holds the instance’s only thread and runs under the 30-second budget, so ship interim results with ws.send() as they arrive, and chunk anything long across messages or drive it from an alarm.

Next Steps

  • Runtime APIwebSocket(), ctx.count(), ctx.broadcast()
  • Connection Lifecycle — duration budget, close codes, reconnect strategy
  • Execution Model — the four guarantees behind single-threaded dispatch
  • Alarms — the delivery contract behind the push pattern
  • Quick Start — scaffold and deploy your first actor

Connection Lifecycle & Limits

Source: https://developers.telnyx.com/docs/edge-compute/stateful-actors/websockets/lifecycle.md
WebSocket support for Stateful Actors is in beta. APIs and limits may change before general availability. One contract underlies everything on this page: every connection ends. A redeploy severs it, the duration budget expires it, a failing handler closes it. Build for that from the start — treat the socket as disposable transport and the actor as the durable thing. Reconnecting is the client’s job; idFromName with the same name routes the new socket back to the same actor, and everything in ctx.storage is exactly where the last connection left it. A client that reconnects with backoff and re-syncs on open loses nothing but the socket.

How a Connection Is Established

Your function’s fetch runs once, at the handshake — check the Upgrade header, authenticate, pick the actor, forward. It never sees another byte of the connection; per-message logic lives in the actor’s webSocket() handler. The full front-door pattern is on the WebSockets page. What a client can rely on at and after the handshake:
  • The upgrade must be a GET. A non-GET request with an Upgrade header is refused with HTTP 400 — no handshake starts.
  • The first client-offered subprotocol is echoed. Offer ["chat.v2", "chat.v1"] and the connection is accepted with chat.v2. The actor cannot select among the offers today; it observes the echoed value as ws.protocol. Put your preferred protocol first.
  • Compression is never negotiated. A permessage-deflate offer is accepted without the extension; frames flow uncompressed.
  • Text and binary frames both work. The handler’s message listener receives (data, isBinary); binary frames round-trip byte-identically.
  • Protocol-level pings are answered by the platform, payload echoed. You don’t write keepalive code on the actor side.

How Connections End

Today, a connection lives at most about five minutes. The total-duration budget applies even to a socket actively exchanging frames; the cutoff surfaces as close code 1006 with no close frame. This is a current limit, not the contract — it will be raised in a later release. Until then, design clients to ride through a reconnect every few minutes as a matter of course. There is no separate idle kill below that budget — a socket that goes quiet for 90 seconds and then sends a frame still gets that frame delivered and handled. The duration budget is what ends it. That guarantee covers the platform’s half of the path only. NATs, corporate proxies, and load balancers between your client and Telnyx run their own idle timers, and any of them can drop a quiet connection long before the duration budget would — often silently, leaving the client holding a socket that looks open but delivers nothing. No close event fires for a drop like that, so reconnect logic alone never notices it; only traffic does. If your clients sit quiet for long stretches, send a periodic heartbeat: non-browser clients can use a protocol-level ping, which the platform answers, and browser WebSocket clients — which cannot send pings — should use a small application-level frame the webSocket() handler replies to. A heartbeat frame is one more frame through the same single-threaded dispatch, so keep it small and infrequent, and give your client library’s own idle or read timeouts the same review. Deploys sever live sockets. Shipping a new version of your function drops every connection it carries as 1006. This is another reason reconnect logic is not optional: your own deploys exercise it.

Reconnecting

A complete client. It reconnects on every close except the deliberate ones — 1000, or an application-defined 4xxx — backs off exponentially, backs off harder on 1013, and re-syncs on open — the socket is new even though the actor and its storage are not.
Reconnect to the same name. idFromName("standup") routes to the same actor every time, so the durable sequence counters, history, and alarms of the conversation are all still there. What does not survive is anything held only in the actor’s instance fields or in the old socket itself — the same rule as the rest of the platform: only ctx.storage is real. See Lifecycle & placement.

Limits

The queue limit is a consequence of single-threaded dispatch: an inbound frame is handled like a method call — one at a time per instance, serialized with RPCs and alarms. While one handler runs, further frames queue; a slow handler with fast senders overflows the queue and the socket closes 1013. Keep per-message work well under the 30-second budget — the queue bound is sized for handlers that finish in milliseconds, not tens of seconds. The same serialization sets the practical ceiling on how many sockets one instance should carry. The table has no socket-count row because the working bound is not a count — it is aggregate frame rate. An instance handles one frame at a time from all of its sockets combined: at one millisecond of handler work per frame, the whole instance drains at most on the order of a thousand inbound frames per second, and a ctx.broadcast() inside a handler writes one frame to every connected socket, so fan-out work grows with the audience. The 256-event queue absorbs a burst of at most a couple hundred frames, not thousands of senders talking at once. When one name’s combined rate approaches what its handlers can drain, partition the workload across names — a room per idFromName, a shard per topic or region — so each instance coordinates only the sockets whose combined traffic it can serialize. An actor is a unit of coordination, not a unit of capacity. The outbound direction is the mirror image, and it is not policed. ws.send() never blocks the handler, and the platform never drops or coalesces frames on a live connection — anything the client hasn’t drained yet waits in the actor’s memory, and ws.bufferedAmount (standard Node ws) reports how much is queued on a socket. Against a responsive client that number stays near zero; against a stalled one — a backgrounded tab, a dead radio link — every ctx.broadcast() tick adds another payload to the stalled socket’s queue. Bound what one slow consumer can cost: keep broadcast payloads small, and when updates outpace a client, send the latest state rather than queueing every delta — durable state in ctx.storage means a client that fell behind can re-sync from a snapshot instead of replaying a backlog. A socket whose bufferedAmount only grows is gone in every way that matters; close it, and the reconnect-and-re-sync logic every client here needs anyway will pick it back up.

What Holds the Actor in Memory

While a connection is open its actor stays resident — the actor is not deactivated between frames — and under today’s duration budget a connection never lives long enough for the idle eviction described in Lifecycle & placement to be a factor. The cost model follows: a million idle actors are cheap; a million actors each holding an open socket are a million resident activations. Teardown is orderly on the actor side. close and error listeners run under the same single-threaded dispatch as any other event, and a ctx.storage write inside a close handler commits — the instance stays resident until those final handlers finish, not merely until the raw socket drops. Recording “last seen” in a close handler is safe. After the last socket’s close handler finishes, the actor is an ordinary idle actor again: evictable, and revivable by the next idFromName call — or by an alarm it set while the connection was up.

Next Steps

  • WebSockets — the front-door pattern, the webSocket() handler, and how messages dispatch
  • Lifecycle & placement — eviction and restart for actors generally
  • Alarms — deferred work that outlives any connection

Network

Network Overview

Source: https://developers.telnyx.com/docs/network/overview.md

Introduction

Telnyx programmable networking allows users and their application to access Telnyx SIP and API endpoints without traversing the public internet, therefore offering a path with better latency and throughput. The SIP and API endpoints are:
  • api.telnyx.com
  • sip.telnyx.com and their regional counterparts
  • *.telnyxcloudstorage.com where * is all available storage regions, e.g. us-central-1
The users’ applications can reside in any one of the major hyperscalers, i.e. AWS, GCP, and Azure, or the users’ own data centers (as long as the user has a presence in Equinix.) This model is analogous to certain AWS concepts — From the “outside” world, a user can use a Direct Connect to connect to private resources (e.g. your EC2 instances) or public resources (e.g. S3). Though to AWS, the “outside” world is largely limited to their users’ on premise data centers.

API Concepts

Coverage

Coverage API enumerates all the supported products at each of Telnyx’s PoP. Before creating any resources, please ensure that
  1. There is a PoP in the vicinity from where you want to connect, and
  2. The desired resources are available at that PoP.
Equally important, depending on the resource, there are product specific coverage endpoints you must consult. Refer to the coverage guide for more detail.

Networks

It is a logically isolated virtual network. It is a pre-requisite for the creation of other elements. Refer to the network guide for more detail.

Gateways

It is a virtual node that facilitates traffic routing between your Telnyx network and an outside network. There are three types of gateways.
  • WireGuard Gateways (WGW); in the API, it’s named interface
  • Internet Gateways (IGW)
  • Private Wireless Gateways (PWG)

WireGuard Gateways

It is a virtual VPN concentrator implemented with the open source WireGuard protocol. It is similar to
  • Virtual Private Gateway in AWS
  • VPN Gateway in GCP
  • Virtual Network Gateway in Azure
Refer to the WireGuard Gateway guide for more detail.

Internet Gateways

It is an element that routes traffic to and from the internet, similar to Internet Gateway or NAT Gateway in AWS Refer to the Internet Gateways Guide

Private Wireless Gateways

It is a private packet gateway that concentrates, routes, and segments traffic for a group of SIMs. Refer to private wireless gateway guide for more detail.

Virtual Cross Connects

It is a virtual direct connection between a supported cloud provider and Telnyx. Refer to VXC guide for more detail.

Coverage

Source: https://developers.telnyx.com/docs/network/coverage.md

Generic Coverage

Individual networking resource availability is not universal across all Telnyx regions. Hence the user must ensure the desired resource is offered at the desired region. To this end, we offer a coverage API (GET /v2/network_coverage). The location parameter is hierarchical: The available_services parameter is an array of all available resources that can be created at this specific location. Possible values are:
  • virtual_cross_connect
  • cloud_vpn aka wireguard interface aka wireguard gateway
  • private_wireless_gateway
  • public_internet_gateway
Here are some common usage patterns of this API.

Querying by the desired product

Querying by the desired product with a location constraint

Querying by the desired location

Here is a sample response:

Virtual Cross Connects (VXC) Coverage

In the case of virtual cross connect (VXC) there are two terminating endpoints to every single connection: a cloud provider endpoint and a Telnyx endpoint. As a result, it’s insufficient to simply perform the coverage check. For example, the above API response only indicates that one end of the VXC can be terminated at the Telnyx’s FR5 PoP. It provides no information about the other terminating endpoint. To that end, we offer a VXC coverage API (GET /v2/virtual_cross_connects_coverage) to augment the generic coverage API. Here are some common usage patterns of this API. Querying by fixing Telnyx endpoint
The response to the above query shows all the possible cloud provider endpoints at which a VXC, whose one end is terminated at Telnyx CH1 PoP, can be terminated. Querying by fixing cloud provider endpoint
The response to the above query shows all the possible Telnyx endpoints at which a VXC, whose one end is terminated at us-east-1 of AWS, can be terminated. Here is a sample response:
The available_bandwidth is in Mbps.

Networks Configuration

Source: https://developers.telnyx.com/docs/network/networks.md
A network instance is a prerequisite for the creation of all other elements. Here is a sample request:

Request sample

Don’t forget to update YOUR_API_KEY here.
Here is a sample response:
API reference is available here.

WireGuard Gateway Setup

Source: https://developers.telnyx.com/docs/network/gateways/wireguard-gateway.md
On naming convention: in the API Reference, certain legacy documentations, and the Mission Control Portal, WireGuard Gateways are referred to as “Wireguard Interfaces”. In this guide and all guides under the “Networking” collection, the term WireGuard Gateway is used consistently. The devices that connect to it are referred to as WireGuard Peers or simply Peers

Introduction

WireGuard Gateway (WGW) is an element to which peers can connect and form a VPN over the public internet. It’s built on top of the WireGuard implementation.

Creating a WGW

Step 1: Create a Network

Follow this guide.

Step 2: Check for Coverage

Follow this guide. Use filter[available_services][contains]=cloud_vpn to look for a desired site at which to deploy the WGW. ashburn-va is chosen for the subsequent steps.

Step 3: Create a WGW

Using the network created from Step 1 and the chosen region from Step 2, create the WGW. Once a WGW is created on a specific network, it cannot be “migrated” to another one; it needs to be recreated on the other network. Double check the correct network_id is used in the following API request.
It’s not yet ready to be used immediately after the creation since its status is provisioning.
  • endpoint denotes the publicly routable IP to which peers will connect over the public internet.
  • server_ip_address is the private subnet range the WGW and peers will use with WGW occupying the first usable IP.

Step 4: Wait for Status Transition

The expected time for status to transition to provisioned is approximately 5 minutes. You can poll the WGW to check for status.

Creating Peers

In the following example, a peer is created for the WGW from the previous section.
In the response, you are given the key pair with which to configure your peer. private_key is only given to you ONCE in the creation API response. It must be stored by you.
Simpler still, you may use the following API to obtain the conf file template.
After inserting the private_key, you can import the conf file on the peer.
[Interface] refers to the local peer.
  • PrivateKey should have the value returned in the private_key parameter.
  • Address is the next available IP in the subnet range. In this case, since this is the first peer, it gets the next IP after the WGW.
[Peer] refers to the WGW created previously.
  • PublicKey is that of the WGW
  • AllowedIPs is the network this peer has access to. See this excellent explanation.
  • Endpoint is the publicly exposed IP to which peers can connect
  • PersistentKeepalive is a default parameter that can be ignored.

Configuring Peers

With the conf file, you can import that onto the host. Here are the environment specific guides:

Use Cases

In this section, we introduce 3 use cases that WGW can accommodate by itself.

Use Case 1: Multi-Cloud Network

WGW can be used as a VPN concentrator server to facilitate traffic between peers. In the following example, we will construct this architecture. Multi Cloud Peers
  • Peer in Cloud A is an EC2 instance in AWS.
  • Peer in Cloud B is a Droplet in DigitalOcean.
It will be demonstrated that it’s extremely trivial to set up a “multicloud” network.

Peer in Cloud A

This EC2 instance runs an apache server but only allows inbound SSH traffic. EC2 Peer As a result, it’s unreachable from the public internet.
However, it’s connected to the WGW we created earlier.

Peer in Cloud B

Similarly, this Droplet instance runs an apache server but only allows inbound SSH traffic. Droplet Peer As a result, it’s unreachable from the public internet.
However, it’s connected to the WGW we created earlier.

Cross Cloud Boundary

Despite of the firewall rule configs in the respective environments, Peer 1 is able to talk to Peer 2 over the WireGuard subnet. From Peer 1
From Peer 2
This is readily extensible to peers running web services in various environments.

Use Case 2: Private Access to Other Telnyx Services

If a WGW is created with enable_sip_trunking, the WGW will enable routing for all Telnyx public API endpoints. Private Access On the peer config, additional routes are added under AllowedIPs.
As a result, traffic between a connected peer to Telnyx API, SIP, and Storage services will flow through the WGW (172.27.0.1) onto the Telnyx network as demonstrated below.
This arrangement is extremely useful in the case where we have to lock down public routing in the event of an extreme DDoS attack.

Use Case 3: Cross-Region Network

The previous architecture can be extended to the following one. Cross Regional Network In this example ---
  • Site A is Telnyx Ashburn
  • Site B is Telnyx Amsterdam
  • Cloud A is AWS
  • Cloud B is Digital Ocean
Traffic between the two peers ride the Telnyx backbone between Ashburn and Amsterdam. Coming Soon.

Other use cases

Costs

  • MRC for each WGW instance is $10.
  • Connected Peers are free of charge.

API References


Linux Configuration

Source: https://developers.telnyx.com/docs/network/wireguard-peer-config/linux.md

Prerequisite

You come from here and have a .conf file ready.

Step 1: Install WireGuard

ubuntu@ip-10-10-11-10:~$ sudo apt install wireguard

Step 2: Bring up the Interface

Assuming the correct config file is located at /etc/wireguard/peer1.conf

Step 3: Validating and Testing

If another peer is connected to the network, it should be pingable.

Next Steps

WireGuard Gateway (WGW) Guide outlines many use cases that may be valuable to you.

macOS Configuration

Source: https://developers.telnyx.com/docs/network/wireguard-peer-config/macos.md

Prerequisite

You come from here and have a .conf file ready.

Step 1: Install WireGuard Client

From here, choose and install the macOS client.

Step 2: Create a Tunnel

“Import Tunnel(s) from File…” Import Tunnel(s) from File... “Activate” "Activate" At this point, it should show “Status: Active” "Active"

Step 3: Validating and Testing

A connected peer should be pingable.

Next Steps

WireGuard Gateway (WGW) Guide outlines many use cases that may be valuable to you.

Windows Configuration

Source: https://developers.telnyx.com/docs/network/wireguard-peer-config/windows.md

Prerequisite

You come from here and have a .conf file ready.

Step 1: Install WireGuard Client

From here, choose and install the Windows Installer.

Step 2: Create a Tunnel

“Import Tunnel(s) from File…” Import Tunnel(s) “Activate” Activate Tunnel At this point, it should show “Status: Active”

Step 3: Validating and Testing

You can try pinging to/from a connected peer. If that does not work you can also try to set up a barebones http server and hit it from the connected peer (or vice versa). An example barebones server in python:
Run the server in one peer (python3 server.py - assuming you saved the above example in a file named server.py) and reach it with a client / browser from the other peer:

Next Steps

WireGuard Gateway (WGW) Guide outlines many use cases that may be valuable to you.

Internet Gateway (IGW)

Source: https://developers.telnyx.com/docs/network/gateways/internet-gateway.md
This is a beta feature

Introduction

Internet Gateway (IGW) is an element that routes traffic between your Telnyx network and the internet. It is intended to be used with certain other elements.

Creating an IGW

Step 1: Create a Network

Follow this guide.

Step 2: Check for Coverage

Follow this guide. Use filter[available_services][contains]=public_internet_gateway to look for a desired site at which to deploy the IGW. frankfurt-de is chosen for the subsequent steps.

Step 3: Create an IGW

Using the network created from Step 1 and the chosen region from Step 2, create the IGW. Once an IGW is created on a specific network, it cannot be “migrated” to another one; it needs to be recreated on the other network. Double check the correct network_id is used in the following API request.
It’s not yet ready to be used immediately after the creation since its status is provisioning.

Step 4: Wait for Status Transition

The expected time for status to transition to provisioned is approximately 10 minutes. You can poll the IGW to check for status.

Use Cases

Use Case 1: Pure play VPN

Internet Gateway (IGW) can be used with WireGuard Gateway (WGW) to create a simple VPN service.
  1. Set up an IGW
  2. Set up a WGW and a peer.
  3. On the peer, amend its config with:
    • DNS — choose your resolver. In the following example, 8.8.8.8 is chosen.
    • AllowedIPs — append with 0.0.0.0/0 so that reachability is beyond the private subnet.
Here is an example config of the peer.
Deactivate and reactivate your WireGuard tunnel. You should see the IGW’s public IP showing up when you query what your public IP is.

Use Case 2: IGW + Private Wireless Gateway (PWG)

Coming Soon.

Costs

  • The monthly recurring cost (MRC) for each instance of IGW is $50.
  • Traffic is not currently metered in beta phase. We will meter traffic in GA phase.

API References


Private Wireless Gateway (PGW)

Source: https://developers.telnyx.com/docs/network/gateways/private-wireless-gateway.md

Introduction

Private Wireless Gateway (PGW) is an element that routes traffic between a group of Telnyx SIMs and other network elements. Refer to this guide for more information on PGW.

Creating a PGW

Step 1: Create a Network

Follow this guide.

Step 2: Check for Coverage

Follow this guide. Use filter[available_services][contains]=private_wireless_gateway to look for a desired site at which to deploy the WGW. Currently, Ashburn, VA is the only supported site. We are working on making more sites available.

Step 3: Create a PGW

Once a PGW is created on a specific network, it cannot be “migrated” to another one; it needs to be recreated on the other network. Double check the correct network_id is used in the following API request.
It’s not yet ready to be used immediately after the creation since its status is provisioning.

Step 4: Wait for Status Transition

The expected time for status.value to transition to provisioned is approximately 15 minutes. You can poll the PGW to check for status.

Step 5: Attach a SIM Group

The PGW is of little use when there is no SIMs are attached to it. Individual SIM must be added to a SIM group which then can be attached to the PGW. This is easily accomplished on the Mission Control Portal. PGW will show the SIM group after a successful attachment.

Use Cases

In the following examples, abbreviations are consistently used for brevity.
  • PWG = Private Wireless Gateway
  • WGW = WireGuard Gateway
  • IGW = Internet Gateway
  • VXC = Virtual Cross Connect

Use Case 1: PWG + WGW

We will construct this architecture which is useful when
  1. Traffic to and from SIMs needs to be segmented and private, and
  2. They do not need high bandwidth connection back to your internal service.
PWG + WGW Architecture In the following example
  • The connected device is an iPhone.
  • It only has access to the peers attached to the WGW; there exists no default route for them.
  • Internal Service Peer is a Droplet on DigitalOcean that runs an toy Apache web server.
  • Admin Peer is a local host.
Detailed WGW and Peer setup guide can be found here. On both peers, ensure that both the WGW subnet and the PGW subnet are on the AllowedIPs of the [Peer] config.
From the internal service peer, we can see there is a route to the iPhone.
From the iPhone, it has access to the internal service peer. Connection to Apache

Use Case 2: PWG + WGW + IGW

The above architecture lacks a crucial functionality --- internet reachability. Though a workaround can be constructed by using one of the peers as an exit node for all default traffic, it’s far better to attach an IGW to the network. PWG + WGW + IGW Architecture Follow the procedure outlined in this guide to set up an IGW on the same network. At this point, the iPhone has internet reachability in addition to the WGW subnet and the PGW subnet. Deice Network Stats Deice Internet Reachability In the above screenshots,
  • the iPhone’s wifi is turned off
  • its “Default Gateway IP” is 100.64.199.1 which is that of the PGW
  • its “External IP” is 64.16.243.172 which is that of the IGW
  • it can ping google.com
Roundtrip ping time is high since in this example, the PGW is in Ashburn US while the IGW is in Frankfurt DE.

Use Case 3: PWG + WGW + IGW + VXC

Currently, the PWG + WGW + IGW + VXC combination only works when WGW, IGW, and VXC are at the Frankfurt Site. We are working on expanding the coverage so it will work universally across all Telnyx sites. In the case where the user’s internal services reside in a cloud provider, the architecture can be augmented yet again. In this case, a VXC can be attached to the network to accommodate this scenario. In the following example, we will establish
  • a VXC from AWS eu-central-1
  • an EC2 in AWS eu-central-1 running a toy Apache server.
For detailed VXC guide on AWS and other providers, please refer to this collection of guides PWG + WGW + IGW + VXC Architecture In AWS’s VPC, we can see the routes from Telnyx’s public APIs AND the SIMs’ private IP ranges are advertised and propagated. Route Table This is also validated by the traceroute output from within the EC2 instance. Latency is high as expected since the EC2 is in Frankfurt while the device is attached to a PGW in Ashburn.
Lastly, validate the connection from the device to EC2. Since the EC2 runs a toy Apache server, open up the HTTP port 80 for the SIM subnet. EC2 Inbound Rule From the iPhone, the server is clearly accessible via private IP. iPhone to Apache

Costs

  • The monthly recurring cost (MRC) for each instance of PGW is $100.
  • Additional costs on SIM and data are applicable as well. Consult this guide for more detail.

API References


VXC Introduction

Source: https://developers.telnyx.com/docs/network/vxc/intro.md
This guide first gives an overview of the product’s availability, costs, and the general API structure. Subsequently, it branches into cloud provider specific guides. The reader should only concern themselves with the section about the cloud provider that’s relevant to their use case.

Coverage & Availability

Source: https://developers.telnyx.com/docs/network/vxc/coverage.md
Refer to the coverage API to ensure the desired resource is offered at the desired region.

Pricing

Source: https://developers.telnyx.com/docs/network/vxc/cost.md
You will be charged by Telnyx AND your cloud provider. Telnyx charges an MRC for each VXC instance according to the following schedule, which may change at our discretion according to the general T&C. We can neither interpret nor advise how your cloud provider may charge your account. Please consult your cloud provider’s documentation.

AWS

GCP

Azure


VXC API

Source: https://developers.telnyx.com/docs/network/vxc/api.md
The API resource in question is /v2/virtual_cross_connects. The following group of parameters needs to be provided regardless of the cloud provider. The rest of the parameters are cloud provider specific. Please consult the subsequent sections.

AWS Direct Connect

Source: https://developers.telnyx.com/docs/network/vxc/aws.md

Architecture

We will construct the following architecture: AWS Architecture

Prerequisites

VPC

This step is performed on AWS. AWS VPC ⚠️ Note: Ensure the intended region is selected. In this example, we are using eu-central-1 as the region. AWS Create VPC

Virtual Private Gateway

This step is performed on AWS. Create and attach a virtual private gateway to the VPC created. AWS Virtual Private Gateway ⚠️ Note: Ensure the intended region is selected. In this example, we are using eu-central-1 as the region. AWS Create Virtual Private Gateway ⚠️ Note: Amazon default ASN value is 64512. You will need this value for VXC creation. Next, attach the virtual private gateway to the VPC created in the first step. AWS Attach Virtual Private Gateway AWS Attach Virtual Private Gateway 2 The state of the virtual private gateway will go from “Detached” to “Attaching” and finally “Attached”.

VPC Route Table

This step is performed on AWS. AWS VPC Route Table 1 AWS VPC Route Table 2 AWS VPC Route Table 3

Internet Gateway

This step is performed on AWS. This is optional. Create and attach an internet gateway to the VPC created. AWS Internet Gateway 1 AWS Internet Gateway 2 AWS Internet Gateway 3 AWS Internet Gateway 4 AWS Internet Gateway 5 AWS Internet Gateway 6

Subnet

This step is performed on AWS. Assuming you don’t have a subnet setup — Choose the VPC created. Subnet CIDR block should be chosen from within the VPC block. AWS Subnet

EC2

This step is performed on AWS. Assuming you don’t have an EC2 instance — spin one up in the same region. Assign the VPC and Subnet created previously. Enable public IP on the instance so you can ssh into it. AWS EC2

Account ID

This step is performed on AWS. ⚠️ You would need this number when you create the Telnyx VXC later. AWS Account ID

Telnyx Network

This step is performed on Telnyx. If you don’t have a network created already, you may follow this guide to create one.

Procedure

Create a VXC resource

This step is performed on Telnyx.
request
Take note of the VXC id returned in the response.

Direct Connect

This step is performed on AWS. If you haven’t accepted the connection from your AWS account within 1 hour of the VXC creation, the VXC would be deleted and you will have to start over. AWS Direct Connect AWS Direct Connect 2

Enable Primary Connection

This step is performed on Telnyx. The connection must be enabled to be used.
request
At this point, the connection on AWS will show as “available”. AWS Enable Primary Connection

Virtual Interface

This step is performed on AWS. The connection created above needs to have an interface to which to connect. AWS Virtual Interface 1 AWS Virtual Interface 2 Interface Parameters Lastly you should see the following – the interface is “available”. AWS Interface Parameters

Validate Connection

This step is performed on AWS. ssh into your EC2 instance and either ping or traceroute to one of the IP in the route table of the VPC. You can see the route went over the VXC instead of the public internet. AWS Validate Connection

Google Cloud Interconnect

Source: https://developers.telnyx.com/docs/network/vxc/gcp.md

Architecture

We will construct the following architecture on Google Cloud. GCP Architecture

Prerequisites

VLAN Attachment

This step is performed on Google GCP VLAN Attachment Parameters:
  • “Partner Interconnect Connection”
  • “Set up unencrypted Interconnect”
GCP VLAN Attachment Parameters GCP VLAN Attachment Parameters 2
  • “Network”: Choose the one you are connecting from
  • “Region”: Choose the one that there is a Telnyx PoP in proximity
  • “MTU”: 8896
GCP VLAN Attachment Parameters 3
  • Create a router or choose an existing one.
GCP VLAN Attachment Parameters 4 GCP VLAN Attachment Parameters 5 GCP VLAN Attachment Parameters 6

Telnyx Network

This step is performed on Telnyx. If you don’t have a network created already, you may follow this guide to create one.

Procedure

Create a VXC resource

This step is performed on Telnyx.

Activate Connection

This step is performed on Google. GCP Activate Connection GCP Activate Connection 2 GCP Activate Connection 3 “Peer ASN”: 63440 (Telnyx) “MD5 Authentication”: primary_bgp_key GCP Activate Connection 4

Update BGP Peering

This step is performed on Telnyx. At this point, the connection is “Down”. We need to take the Google assigned GBP IPs and set them on the VXC. GCP Update BGP Peering 1 GCP Update BGP Peering 2 Use the PATCH method.
  • primary_cloud_ip — “Cloud Router BGP IP” or “Remote IP” in the “Troubleshooting” page
  • primary_telnyx_ip — “BGP peer IP” or “Local IP”in the “Troubleshooting” page
At this point, on Google, it will show the “Status” as “Up” GCP Update BGP Peering 3

Validate Connection

This step is performed on Google.
  • Under VPC networks, you should see the routes advertised over the cloud router created.
  • You can also ssh in one of your instances in the same network and perform a traceroute.
GCP Verify Connection

Azure ExpressRoute

Source: https://developers.telnyx.com/docs/network/vxc/azure.md

Architecture

We will construct the following architecture. Azure Architecture

Prerequisites

Create ExpressRoute Circuit

This step is performed on Azure. Azure Prerequisite 1 Azure Prerequisite 2 Choose the rest of the parameters at your own discretion. Azure Pre-requisite 3 At the end of this step, the “Provider status” should say “Not provisioned”.

Telnyx Network

This step is performed on Telnyx. If you don’t have a network created already, you may follow this guide to create one.

Procedure

Create a VXC resource

This step is performed on Telnyx.
request
Take note of the following info in the response; you will need it later.
  • id
  • primary_cloud_ip
  • primary_bgp_key

Enable Primary Connection

This step is performed on Telnyx.
request
You must make sure the VXC status is provisioned before proceeding to the next step. You can poll the status using GET request on the specific VXC.

Azure Private Peering

This step is performed on Azure. “Provider status” must show “Provisioned” before performing this step. Azure Private Peering 1 Azure Private Peering 2 Azure Private Peering 3 After saving, you may need to wait for some time before performing the next step. Azure Private Peering 4 At this point, you should see Telnyx IPs advertised in the result after you click into “View route table.” Azure Private Peering 5 At this point, you are all set. Perform the following steps only if you are trying to test things out before putting it into your own production environment.

Virtual Network

Azure Virtual Network 1 Next, add a Gateway subnet. Keep all parameters as default. Azure Virtual Network 2

Virtual Network Gateways

Azure Virtual Network Gateways 1 The gateway creation will take a while; upward of 20 minutes. In the meantime, you can proceed to the following step.

Virtual Machines

Azure Virtual Machines 1 Azure Virtual Machines 2 You can keep the rest default or choose at your discretion.

Add Gateway Connection

This step is performed on Azure. The Virtual Gateway you created must be successfully deployed before this step is performed. Azure Add Gateway Connection 1 Azure Add Gateway Connection 2 Azure Add Gateway Connection 3

Validate Connection

This step is performed on Azure. Ensure all of the following are successfully deployed:
  • ExpressRoute
  • Virtual network
  • Virtual network gateway & connection
  • Virtual machine
SSH into the VM and perform a traceroute to sip.telnyx.com and sip.telnyx.eu. You can see the next hop is the next hop indicated on the Azure Private Peering under the express route. Azure Validate Connection

API Reference (Compute)

Regions

Networks

WireGuard Interfaces

Private Wireless Gateways

Public Internet Gateways

Virtual Cross Connects

Global IPs