A channel adapter is any process that speaks a 3-endpoint HTTP protocol. The gateway never imports adapter code. To add a new platform — SMS, Slack, IRC, a proprietary API — you run a process that implements three endpoints, calls one registration endpoint at startup, and posts inbound messages as JSON. That is the entire contract.
Your adapter must expose two HTTP endpoints and call one on the gateway:
| Direction | Endpoint | Who calls it |
|---|---|---|
| Outbound delivery | POST /send on your adapter | Gateway calls this to deliver a message to the platform |
| Health check | GET /health on your adapter | Gateway polls this to track liveness |
| Registration | POST /v1/channels/register on the gateway | Your adapter calls this once at startup |
At startup, POST to http://<gateway>:8080/v1/channels/register:
{
"name": "myplatform",
"url": "http://myadapter:9005",
"prefixes": ["myplatform:"],
"capabilities": ["send_text", "send_image"]
}
name is the internal identifier. prefixes declares what JID namespaces this adapter owns — the gateway uses this for prefix-scan routing when no JID→adapter mapping exists yet. capabilities is informational.
When a user sends a message on your platform, POST to the gateway's inbound endpoint:
POST /v1/messages
{
"chat_jid": "myplatform:channel123",
"sender_jid": "myplatform:user456",
"content": "hello",
"adapter": "myplatform",
"timestamp": 1711500000
}
Use chanlib.RouterClient to handle auth headers and retries:
rc := chanlib.NewRouterClient(routerURL, channelSecret)
err := rc.PostMessage(ctx, chanlib.InboundMessage{
ChatJID: "myplatform:channel123",
SenderJID: "myplatform:user456",
Content: msg.Text,
Adapter: "myplatform",
})
package main
import (
"encoding/json"
"net/http"
"os"
"github.com/onvos/pub/arizuko/chanlib"
)
type Adapter struct {
rc *chanlib.RouterClient
}
// handleSend is called by the gateway to deliver a message to the platform.
func (a *Adapter) handleSend(w http.ResponseWriter, r *http.Request) {
var msg chanlib.OutboundMsg
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
http.Error(w, err.Error(), 400)
return
}
// deliver msg.Content to msg.ChatJID on your platform
// e.g. call your platform's API here
w.WriteHeader(200)
}
func (a *Adapter) handleHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}
func main() {
routerURL := os.Getenv("ROUTER_URL") // e.g. http://gated:8080
secret := os.Getenv("CHANNEL_SECRET")
listenURL := os.Getenv("LISTEN_URL") // e.g. http://myadapter:9005
rc := chanlib.NewRouterClient(routerURL, secret)
a := &Adapter{rc: rc}
mux := http.NewServeMux()
mux.HandleFunc("/send", a.handleSend)
mux.HandleFunc("/health", a.handleHealth)
// register with the gateway
if err := rc.Register(chanlib.Registration{
Name: "myplatform",
URL: listenURL,
Prefixes: []string{"myplatform:"},
Capabilities: []string{"send_text"},
}); err != nil {
panic(err)
}
http.ListenAndServe(":9005", mux)
}
That is ~60 lines including imports. The adapter does not need to know anything about groups, routing, or MCP. It delivers messages and receives deliveries.
Add a service TOML in your instance's services/ directory. The compose generator picks it up:
# /srv/data/arizuko_myinstance/services/myplatform.toml
image = "myplatform:latest"
[environment]
ROUTER_URL = "http://gated:8080"
CHANNEL_SECRET = "${CHANNEL_SECRET}"
LISTEN_URL = "http://myplatform:9005"
LISTEN_ADDR = ":9005"
Re-run arizuko generate myinstance and the new service appears in docker-compose.yml.
chanreg.Registry is a name→URL map maintained in memory. When the gateway needs to deliver an outbound message, HTTPChannel.Owns(jid) does a prefix scan across registered adapters. On first inbound message, the gateway calls RecordJIDAdapter(chatJID, adapterName) — subsequent outbound messages to that JID route directly without the prefix scan.
The gateway has zero imports from any adapter package. core.Channel is an interface with two methods: Send and Owns. HTTPChannel implements that interface by calling your adapter's /send endpoint. This is why the WhatsApp adapter is TypeScript and every other adapter is Go — the protocol is the contract, not the language.
Adding a new platform does not touch any gateway code. The registration payload is the only configuration.