> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Power AI Assistants with Edge Compute

> Use a Telnyx Edge Compute function as the backend for dynamic variables and webhook tool calls — no separate server required.

Telnyx AI Assistants can call out to your own backend in several scenarios — resolving dynamic variables at the start of a conversation, executing webhook tool calls mid-conversation, and more. Whenever you need a backend for these callbacks, Telnyx Edge Compute is a natural fit: no server to manage, secrets injected at runtime, and deployment via a single CLI command.

This guide walks through building a single Go function that handles both dynamic variables and webhook tool calls, using the demo app `telnyx-ai-edge` as the reference implementation.

***

## What you'll build

A support assistant for "Telnyx Logistics" that:

* Greets callers by name (dynamic variables resolved from the caller's phone number)
* Has a `lookup-order` tool the assistant can call to retrieve order status, carrier, and estimated delivery

Both the dynamic variable lookup and the tool call hit one Edge Compute function at a single URL.

***

## Prerequisites

* A Telnyx account with [Edge Compute](/docs/edge-compute/quickstart) enabled.
* The `telnyx-edge` CLI [installed and authenticated](/docs/edge-compute/quickstart).
* An existing [AI Assistant](https://portal.telnyx.com/#/ai/assistants) (or you can create one via API as shown below).
* Go 1.24+ installed locally (if following along with the Go sample).

***

## Key concepts

### Single function, two callbacks

Edge Compute routes all HTTP methods and paths under your function URL to your handler — path handling is up to your code (see [Routes & Domains](/docs/edge-compute/configuration/routing)). The platform handles `/health/liveness` and `/health/readiness` probes automatically.

In this guide, both the dynamic variables webhook and the webhook tool call point to the same function URL, so the handler dispatches on the **request body shape** rather than the URL path:

* **Dynamic variables webhook** — Telnyx wraps the payload under `data.event_type`.
* **Webhook tool call** — the body is the flat arguments object from the tool's `body_parameters` schema (e.g. `{"order_id": "ORD-10042"}`).

You could also use separate paths (e.g. `/dynamic-variables` and `/tool/lookup-order`) if you prefer path-based routing — both approaches work. This guide uses body-shape dispatch to keep everything at a single URL.

### Webhook signature verification

Telnyx signs every dynamic-variables webhook and webhook tool call with an Ed25519 key. The signature is in the `telnyx-signature-ed25519` header, and the timestamp is in `telnyx-timestamp`. The signed message is `"{timestamp}|{raw_body}"`.

You must verify this signature to confirm the request is genuinely from Telnyx. Your org's public key is available at:

```
GET https://api.telnyx.com/v2/public_key
Authorization: Bearer <TELNYX_API_KEY>
```

The response contains `data.public` (not `data.public_key`) — the base64-encoded Ed25519 public key.

### Dynamic variables response format

The response **must** nest variables under a `dynamic_variables` key. A flat object (e.g. `{"customer_name": "James"}`) is silently ignored — variables will remain unresolved.

```json theme={null}
{
  "dynamic_variables": {
    "customer_name": "James Smith",
    "account_tier": "premium"
  }
}
```

### Timeout

The default dynamic variables webhook timeout is 1,500 ms. Edge Compute functions may occasionally need more time on a cold start, so consider setting `dynamic_variables_webhook_timeout_ms` on the assistant to a higher value (up to 10,000 ms). A value of 8,000 ms is a reasonable choice for edge backends.

***

## Step 1: Scaffold the function

```bash theme={null}
telnyx-edge new-func -l go -n telnyx-ai-edge
cd telnyx-ai-edge
```

This creates a `func.toml` with the registered function ID and a Go handler scaffold.

<Note>
  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`.
</Note>

***

## Step 2: Store the public key as a secret

Fetch your org's public key and store it as an encrypted secret. The public key endpoint requires authentication — use your Telnyx API key:

```bash theme={null}
# Get the public key (requires authentication)
PUBLIC_KEY=$(curl -s -H "Authorization: Bearer $TELNYX_API_KEY" \
  https://api.telnyx.com/v2/public_key | jq -r '.data.public')

# Store it as a secret (encrypted, org-scoped, injected as env var at runtime)
telnyx-edge secrets add TELNYX_PUBLIC_KEY "$PUBLIC_KEY"
```

The function reads this secret from `os.Getenv("TELNYX_PUBLIC_KEY")` at startup. Secrets are never visible in `secrets list` — only the name is shown.

***

## Step 3: Write the handler

The handler does three things:

1. Verifies the Telnyx Ed25519 signature on every request
2. Detects whether the request is a dynamic-variables webhook or a tool call
3. Returns the appropriate response

```go handler.go theme={null}
package function

import (
	"crypto/ed25519"
	"encoding/base64"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

const maxSkew = 5 * time.Minute

var publicKey ed25519.PublicKey

func init() {
	raw := os.Getenv("TELNYX_PUBLIC_KEY")
	if raw == "" {
		log.Println("warning: TELNYX_PUBLIC_KEY is not set; all requests will be rejected")
		return
	}
	key, err := base64.StdEncoding.DecodeString(raw)
	if err != nil || len(key) != ed25519.PublicKeySize {
		log.Printf("warning: TELNYX_PUBLIC_KEY is invalid (len=%d, err=%v)", len(key), err)
		return
	}
	publicKey = ed25519.PublicKey(key)
}

func Handle(w http.ResponseWriter, r *http.Request) {
	// Health probes are handled by the platform — don't add custom health routes
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "cannot read body", http.StatusBadRequest)
		return
	}

	if !verifyTelnyxSignature(r.Header, body) {
		http.Error(w, "invalid signature", http.StatusForbidden)
		return
	}

	// Dispatch on body shape: DV webhook has "data.event_type",
	// tool call is a flat args object
	if isDynamicVariablesRequest(body) {
		handleDynamicVariables(w, body)
		return
	}
	handleLookupOrder(w, body)
}

func isDynamicVariablesRequest(body []byte) bool {
	var probe struct {
		Data *struct {
			EventType string `json:"event_type"`
		} `json:"data"`
	}
	if err := json.Unmarshal(body, &probe); err != nil {
		return false
	}
	return probe.Data != nil
}

func verifyTelnyxSignature(h http.Header, body []byte) bool {
	if publicKey == nil {
		return false
	}

	sig := h.Get("telnyx-signature-ed25519")
	ts := h.Get("telnyx-timestamp")
	if sig == "" || ts == "" {
		return false
	}

	t, err := strconv.ParseInt(ts, 10, 64)
	if err != nil {
		return false
	}
	age := time.Since(time.Unix(t, 0))
	if age < -maxSkew || age > maxSkew {
		return false
	}

	s, err := base64.StdEncoding.DecodeString(sig)
	if err != nil {
		return false
	}

	signed := append([]byte(ts+"|"), body...)
	return ed25519.Verify(publicKey, signed, s)
}

// --- Dynamic Variables ---

type dvRequest struct {
	Data struct {
		EventType string `json:"event_type"`
		Payload   struct {
			Channel       string `json:"telnyx_conversation_channel"`
			AgentTarget   string `json:"telnyx_agent_target"`
			EndUserTarget string `json:"telnyx_end_user_target"`
			CallControlID string `json:"call_control_id"`
			AssistantID   string `json:"assistant_id"`
		} `json:"payload"`
	} `json:"data"`
}

type dvResponse struct {
	DynamicVariables map[string]any `json:"dynamic_variables"`
}

func handleDynamicVariables(w http.ResponseWriter, body []byte) {
	var req dvRequest
	if err := json.Unmarshal(body, &req); err != nil {
		http.Error(w, "bad json", http.StatusBadRequest)
		return
	}

	caller := req.Data.Payload.EndUserTarget

	resp := dvResponse{
		DynamicVariables: map[string]any{
			"customer_name":  lookupCustomerName(caller),
			"account_tier":   "premium",
			"open_order_id":  "ORD-10042",
			"support_region": "US",
		},
	}

	writeJSON(w, resp)
}

func lookupCustomerName(caller string) string {
	known := map[string]string{
		"+13128675309": "James Smith",
		"+15551234567": "Rachel Thomas",
	}
	if name, ok := known[caller]; ok {
		return name
	}
	return "there"
}

// --- Webhook Tool: lookup-order ---

type toolRequest struct {
	OrderID string `json:"order_id"`
}

type toolResponse struct {
	OrderID        string `json:"order_id"`
	Status         string `json:"status"`
	EstimatedDeliv string `json:"estimated_delivery"`
	Carrier        string `json:"carrier"`
}

func handleLookupOrder(w http.ResponseWriter, body []byte) {
	var req toolRequest
	if err := json.Unmarshal(body, &req); err != nil {
		http.Error(w, "bad json", http.StatusBadRequest)
		return
	}

	resp := toolResponse{
		OrderID:        req.OrderID,
		Status:         "shipped",
		EstimatedDeliv: "2025-04-10",
		Carrier:        "Telnyx Logistics",
	}

	writeJSON(w, resp)
}

func writeJSON(w http.ResponseWriter, v any) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(v)
}
```

***

## Step 4: Ship the function

```bash theme={null}
telnyx-edge ship
```

The ship process takes 2–3 minutes. After `uploaded successfully`, poll `telnyx-edge list` until the status shows `deploy_ok`. Don't trust a CLI timeout as a failure — the function may still be building server-side.

```bash theme={null}
telnyx-edge list
# FUNC ID                          FUNCTION NAME    STATUS      INVOKE URL
# 917210e7-...                     telnyx-ai-edge   deploy_ok  https://telnyx-ai-edge-<org>.telnyxcompute.com
```

Save the invoke URL — you'll point the assistant at it next.

***

## Step 5: Configure the AI Assistant

Set the function URL as both the dynamic variables webhook URL and the webhook tool URL on the assistant.

### Dynamic variables webhook

In the [Portal](https://portal.telnyx.com/#/ai/assistants) or via the API:

| Field                                  | Value                                             |
| -------------------------------------- | ------------------------------------------------- |
| `dynamic_variables_webhook_url`        | `https://telnyx-ai-edge-<org>.telnyxcompute.com/` |
| `dynamic_variables_webhook_timeout_ms` | `8000`                                            |

Consider setting the timeout to 8,000 ms to give the function room on cold starts. The default 1,500 ms may be tight for a cold function.

### Template variables in the assistant

Use `{{variable_name}}` in the assistant's instructions and greeting to reference the variables your function returns:

```
instructions: "You are a support agent for Telnyx Logistics. The caller is {{customer_name}} (tier: {{account_tier}}). They may have open order {{open_order_id}}."
greeting: "Hi {{customer_name}}, thanks for calling Telnyx Logistics. How can I help you today?"
```

### Webhook tool

Add a webhook tool that points to the same function URL:

```json theme={null}
{
  "type": "webhook",
  "webhook": {
    "name": "lookup-order",
    "description": "Look up the current status of a customer order by its order id.",
    "url": "https://telnyx-ai-edge-<org>.telnyxcompute.com/",
    "method": "POST",
    "body_parameters": {
      "type": "object",
      "properties": {
        "order_id": {
          "type": "string",
          "description": "The order id to look up, e.g. ORD-10042."
        }
      },
      "required": ["order_id"]
    }
  }
}
```

When the LLM decides to call `lookup-order`, Telnyx sends a POST with the tool arguments as the flat body (`{"order_id": "ORD-10042"}`), signed with the same Ed25519 key. Your function detects the body shape, handles it as a tool call, and returns the result.

***

## Step 6: Test end-to-end

1. **Call the function directly** (without a signature — it'll return 403, confirming it's live):
   ```bash theme={null}
   curl -X POST https://telnyx-ai-edge-<org>.telnyxcompute.com/ \
     -H "Content-Type: application/json" \
     -d '{"order_id":"ORD-10042"}'
   # → 403 invalid signature  ← expected, signature verification is working
   ```

2. **Make a test call** to the assistant from the Portal or via the API:
   ```bash theme={null}
   curl --request POST \
     --url https://api.telnyx.com/v2/texml/ai_calls/<texml_app_id> \
     --header "Authorization: Bearer $TELNYX_API_KEY" \
     --header 'Content-Type: application/json' \
     --data '{
       "From": "+13128675309",
       "To": "+15551234567",
       "AIAssistantId": "assistant-<id>"
     }'
   ```

3. **Verify in the conversation transcript** that:
   * The greeting includes the resolved `customer_name`
   * The assistant can call `lookup-order` and read back real order data

***

## Tips and gotchas

### Choosing body-shape vs path-based dispatch

Since Edge Compute routes all paths to your handler, you can use path-based routing (e.g. `r.URL.Path == "/tool/lookup-order"`) or body-shape dispatch as shown in this guide. Both work. If you configure separate URLs for the DV webhook and the tool on the assistant, path-based routing is natural. If you point both at the same URL, body-shape dispatch is the way to go.

For a path-based routing example, see the [RESTful API example](https://github.com/team-telnyx/edge-compute-cli/tree/main/docs/examples/python/restful-api) in the Edge Compute CLI repo.

### Consider a higher webhook timeout

The default dynamic variables webhook timeout is 1,500 ms. Edge Compute functions may occasionally need a bit more time on a cold start, so consider setting `dynamic_variables_webhook_timeout_ms` to 8,000 ms to give the function room. The maximum is 10,000 ms.

### Always verify signatures

Without signature verification, anyone who knows your function URL can inject fake dynamic variables or tool responses. The `telnyx-signature-ed25519` and `telnyx-timestamp` headers are present on every request from Telnyx.

### Ship takes a few minutes

A normal ship takes 2–3 minutes. The CLI's build monitor has a 5-minute timeout, but the build continues server-side regardless. If the CLI reports a timeout, check `telnyx-edge list` for the actual status before retrying — the function may have deployed successfully.

### Secrets require re-shipping

Adding or changing a secret (`telnyx-edge secrets add`) does not affect an already-deployed function. Run `telnyx-edge ship` again to pick up the new secret.

### The `dynamic_variables` wrapper is mandatory

Returning a flat JSON object like `{"customer_name": "James"}` will be silently ignored. Variables must be nested under `dynamic_variables`:

```json theme={null}
{
  "dynamic_variables": {
    "customer_name": "James"
  }
}
```

***

## Next steps

* [Dynamic Variables](/docs/inference/ai-assistants/dynamic-variables) — full reference for the DV webhook payload and resolution precedence.
* [Webhook signing](/development/api-fundamentals/webhooks/receiving-webhooks#webhook-signing) — how Telnyx signs webhooks and how to verify signatures.
* [Edge Compute quickstart](/docs/edge-compute/quickstart) — getting started with your first function.
* [Secrets](/docs/edge-compute/configuration/secrets) — encrypted, org-scoped environment variables.
* [Bindings](/docs/edge-compute/runtime/bindings) — pre-authenticated Telnyx API client for your function.
