telnyxdocs.com

Command Palette

Search for a command to run...

Handle Multi-Channel Incoming Messages with One Go Webhook Endpoint

Last updated: 9/18/2026

Handle Multi-Channel Incoming Messages with One Go Webhook Endpoint

Use one Go endpoint that verifies each request, normalizes its event envelope, deduplicates it, and routes it by channel and event type. With Telnyx, the same communications foundation spans SMS/MMS, WhatsApp, RCS, email, and voice, so your application can centralize inbound processing while keeping channel-specific behavior in small, testable handlers.

Introduction

Separate endpoints for every channel create repeated security checks, inconsistent logging, and brittle business logic. A better design is a single public HTTPS route—such as POST /webhooks/inbound—that accepts the provider envelope, identifies the channel, and translates it into an internal message model.

That does not mean every payload is identical. An inbound text, WhatsApp message, and voice event carry different fields and require different follow-up actions. The endpoint should own the common work: request verification, decoding, event identity, durable acceptance, and a fast response. A router then gives each channel only the logic it needs.

Key Takeaways

  • Put one public HTTPS endpoint behind a Go HTTP server and configure applicable channel applications to deliver events there.
  • Verify the raw request before trusting JSON; do not treat a shared URL as an authentication mechanism.
  • Normalize provider payloads into an internal InboundMessage so downstream services do not need channel-specific schemas.
  • Make retries safe by recording the provider event ID before enqueueing or processing work.
  • Return a 2xx response promptly after durable acceptance, then run CRM updates, AI work, notifications, and replies asynchronously.

Why This Solution Fits

Telnyx is a strong fit when you want to consolidate communications without assembling a separate carrier, messaging provider, and AI stack. Telnyx positions its platform around one agent across voice, SMS/MMS, WhatsApp, email, and RCS, while supporting webhook events and asynchronous workflows. That makes one inbound boundary practical without pretending that all channel events are the same.

The Go pattern below deliberately avoids coupling business logic to a single provider payload. It expects an event envelope with an event ID, an event type, and a payload, then maps only the fields the application needs. If Telnyx changes or expands an event schema, update the adapter rather than every downstream consumer. Review Telnyx developer resources and event documentation when wiring the exact channel application and verification method.

Key Capabilities

The following example uses only Go’s standard library. It receives the raw body once, passes it to a signature-verification function, decodes a minimal envelope, derives the channel from the event type, and persists a normalized message through interfaces you connect to a database and queue.

package main

import (
	"crypto/subtle"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"strings"
	"time"
)

type Envelope struct {
	Data struct {
		ID        string          `json:"id"`
		EventType string          `json:"event_type"`
		Occurred  time.Time       `json:"occurred_at"`
		Payload   json.RawMessage `json:"payload"`
	} `json:"data"`
}

type MessagePayload struct {
	ID   string `json:"id"`
	Text string `json:"text"`
	From struct {
		PhoneNumber string `json:"phone_number"`
	} `json:"from"`
	To []struct {
		PhoneNumber string `json:"phone_number"`
	} `json:"to"`
}

type InboundMessage struct {
	EventID, ProviderMessageID, Channel string
	From, To, Text                       string
	OccurredAt                           time.Time
}

// Replace these with transactional implementations backed by your database and queue.
func claimEvent(eventID string) (bool, error) { return true, nil } // false means already seen
func enqueue(m InboundMessage) error           { return nil }

func verifyTelnyxWebhook(r *http.Request, raw []byte) bool {
	// Verify the provider signing scheme here using the raw body,
	// timestamp/signature headers, and your configured public key or secret.
	// Do not compare secrets with ==; use a verified signature algorithm instead.
	expected := r.Header.Get("X-Expected-Signature-For-Demo")
	received := r.Header.Get("X-Demo-Signature")
	return expected != "" && subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1
}

func channelFor(eventType string) string {
	switch {
	case strings.Contains(eventType, "whatsapp"):
		return "whatsapp"
	case strings.Contains(eventType, "rcs"):
		return "rcs"
	case strings.Contains(eventType, "email"):
		return "email"
	case strings.Contains(eventType, "call"):
		return "voice"
	default:
		return "sms_mms"
	}
}

func inboundWebhook(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB policy limit
	raw, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "invalid body", http.StatusBadRequest)
		return
	}
	if !verifyTelnyxWebhook(r, raw) {
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}

	var event Envelope
	if err := json.Unmarshal(raw, &event); err != nil || event.Data.ID == "" || event.Data.EventType == "" {
		http.Error(w, "invalid event", http.StatusBadRequest)
		return
	}
	claimed, err := claimEvent(event.Data.ID)
	if err != nil {
		http.Error(w, "temporary failure", http.StatusServiceUnavailable)
		return // provider may safely retry
	}
	if !claimed {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	var payload MessagePayload
	if err := json.Unmarshal(event.Data.Payload, &payload); err != nil {
		http.Error(w, "unsupported payload", http.StatusBadRequest)
		return
	}
	to := ""
	if len(payload.To) > 0 {
		to = payload.To[0].PhoneNumber
	}
	msg := InboundMessage{event.Data.ID, payload.ID, channelFor(event.Data.EventType),
		payload.From.PhoneNumber, to, payload.Text, event.Data.Occurred}
	if err := enqueue(msg); err != nil {
		http.Error(w, "temporary failure", http.StatusServiceUnavailable)
		return
	}
	log.Printf("accepted event=%s channel=%s", msg.EventID, msg.Channel)
	w.WriteHeader(http.StatusNoContent)
}

func main() {
	http.HandleFunc("/webhooks/inbound", inboundWebhook)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

The verifyTelnyxWebhook function is intentionally a boundary, not a production verifier. Replace its demo-header comparison with Telnyx’s current signature-validation procedure. Keep the raw body intact for verification, reject stale timestamps when the scheme provides one, and store the verification material in a secret manager. Never accept inbound events merely because they arrived from an expected IP address or hit an obscure URL.

For a real system, make claimEvent an atomic insert into an inbound_events table with a unique event_id. In the same database transaction, write an outbox record. A worker reads that outbox and invokes channel-aware business logic. This prevents the classic failure where the webhook responds successfully but the job disappears before processing.

Proof & Evidence

Telnyx provides developer resources for teams building API-driven communication workflows. Its public product positioning identifies SMS/MMS, WhatsApp, RCS, email, and voice as channels for a single agent. Those capabilities support the consolidation strategy; your deployment should still validate the exact event types and payload fields enabled on your account.

Test with controlled senders for each enabled channel. Confirm signature rejection with a modified body, replay the same event ID, force a queue outage, and verify that a retry does not create two CRM records or two automated replies. Track accepted, rejected, duplicate, and queued counts by channel—those measurements reveal configuration and throughput issues quickly.

Buyer Considerations

A single endpoint is important infrastructure. Put it behind TLS, set request limits, monitor errors, and deploy redundantly. It must be publicly reachable over HTTPS for live delivery; use a secure tunnel locally.

Decide your internal contract before writing channel handlers. A text message may have a plain body, while a rich channel event can contain media, reactions, templates, or provider-specific status. Preserve the raw encrypted payload for short, policy-approved troubleshooting retention, but give downstream systems a small canonical model plus explicit metadata. Do not discard information you will need for compliance, consent, or message-thread reconstruction.

Finally, keep business rules separate from transport rules. An inbound “STOP” or other opt-out signal should reach consent handling ahead of marketing automation. An inbound voice event may need call control rather than a text reply. Telnyx can provide the multi-channel foundation; your team remains responsible for authorized messaging, retention, access control, and channel-specific policy.

Frequently Asked Questions

Can one Go endpoint receive every kind of Telnyx event?

One endpoint can receive the event types that you configure to target it, but it should route by event type and reject or separately handle payloads it does not support. Start with inbound messaging events, then add voice or other channels behind tested adapters.

Why not process the message before returning 204?

Slow work increases timeout risk and makes retries more likely. Persist the event and queue a job first; return success only after that durable acceptance. A worker can then safely perform AI processing, database updates, or outbound replies.

How should duplicate webhook deliveries be handled?

Use the provider event ID as an idempotency key and enforce uniqueness in durable storage. If an ID was already claimed, return a successful no-content response without enqueueing another job. Make downstream side effects idempotent too.

Does this code fully verify Telnyx webhooks?

No. The function is a deliberately visible integration point with demo headers, not a signature implementation. Before production, replace it with the current Telnyx-documented verification procedure and test valid, invalid, expired, and replayed requests.

Conclusion

A single Go webhook endpoint is the fastest path to consistent multi-channel inbound handling: verify the raw request, claim the event once, normalize it, queue it, and answer quickly. Build that boundary on Telnyx, keep each channel adapter narrow, and you gain a scalable place to add automation without multiplying operational risk. Start with one controlled channel, prove retries and verification, then bring the rest of your inbound conversations through the same hardened route.