Developer guideiMessage API7 min read
iMessage API for Go
Use an iMessage API from Go with the standard library. Poll events, reply with idempotency keys and verify signed webhooks by hand. Code from the docs included.

The short answer
Flow has no Go SDK yet, but the API is plain HTTP, so Go needs only the standard library. Poll the event log, reply with the event ID as the idempotency key, and verify webhook signatures with HMAC-SHA256 by hand.
Building on the iMessage API? Build on iMessage with Flow: one agent for iMessage, WhatsApp and Telegram. New to it? Read the iMessage API guide.
The short version
Go is a good fit for a messaging worker, and you do not need an SDK for it. Flow’s docs say plainly that the Go SDK is not published yet and show the same agent over plain HTTP with the standard library.
You need a key. Get a test key with no account:
curl -X POST https://api.flow.engineer/v1/sandbox/keys
export FLOW_MESSAGING_KEY=fk_test_...
The test key runs on the Telegram sandbox bot. The same code answers on Flow’s iMessage numbers.
Poll and reply
This is the example from Flow’s quickstart. It reads message.received events from the event log, oldest first, and replies to each with the event ID as the idempotency key:
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"time"
)
const api = "https://api.flow.engineer"
type event struct {
ID string `json:"id"`
Conversation struct {
ID string `json:"id"`
} `json:"conversation"`
Data struct {
Message struct {
Content struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
} `json:"message"`
} `json:"data"`
}
func call(method, path string, body any, idempotencyKey string, out any) error {
var buf bytes.Buffer
if body != nil {
if err := json.NewEncoder(&buf).Encode(body); err != nil {
return err
}
}
req, err := http.NewRequest(method, api+path, &buf)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("FLOW_MESSAGING_KEY"))
req.Header.Set("Flow-Version", "2026-11-01")
req.Header.Set("Content-Type", "application/json")
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 300 {
return fmt.Errorf("%s %s: %s", method, path, res.Status)
}
if out != nil {
return json.NewDecoder(res.Body).Decode(out)
}
return nil
}
func main() {
after := ""
for {
q := url.Values{"type": {"message.received"}, "limit": {"100"}}
if after != "" {
q.Set("after", after)
}
var page struct {
Data []event `json:"data"`
HasMore bool `json:"has_more"`
}
if err := call("GET", "/v1/events?"+q.Encode(), nil, "", &page); err != nil {
fmt.Println(err)
time.Sleep(2 * time.Second)
continue
}
for _, ev := range page.Data {
after = ev.ID
reply := map[string]any{
"content": map[string]any{"type": "text", "text": "You said: " + ev.Data.Message.Content.Text},
}
if err := call("POST", "/v1/conversations/"+ev.Conversation.ID+"/messages", reply, ev.ID, nil); err != nil {
fmt.Println(err)
}
}
if !page.HasMore {
time.Sleep(time.Second)
}
}
}
Notes on this worker:
- Resume point.
afteris the ID of the last event handled. Save it to disk if you want a restart to pick up where it stopped. Events stay in the log for 30 days by default. - Retries are safe. A repeat with the same idempotency key returns the first answer and does not send twice.
- Swap the echo for your model. Call it between reading the text and posting the reply.
Verify a signed webhook
For production you will likely use webhooks. The Go SDK is not there, so check the signature by hand. This is the verification function from Flow’s docs:
func verify(body []byte, header, secret string) bool {
var ts string
var sigs []string
for _, part := range strings.Split(header, ",") {
k, v, _ := strings.Cut(strings.TrimSpace(part), "=")
switch k {
case "t":
ts = v
case "v1":
sigs = append(sigs, v)
}
}
t, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return false
}
if d := time.Now().Unix() - t; d > 300 || d < -300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte("t." + ts + "."))
mac.Write(body)
want := hex.EncodeToString(mac.Sum(nil))
for _, s := range sigs {
if hmac.Equal([]byte(want), []byte(s)) {
return true
}
}
return false
}
It needs crypto/hmac, crypto/sha256, encoding/hex, strconv, strings and time. Read the body with io.ReadAll and pass those exact bytes. A parsed and re-serialized body will not match. The header can carry more than one v1 value while a secret is being rotated, and the function accepts any match.
iMessage webhooks: signing, retries and ordering explains the retries and ordering behind this.
Go with Flow
Flow offers plain HTTP in Go, with TypeScript and Python SDKs for the other common runtimes. The API stays the same across languages, so your agent logic and webhook handling do not change when you move from the sandbox to a live iMessage line.
If an official Go SDK is a requirement, compare the providers. Other languages: Node.js and Python.
Frequently asked questions
Is there an iMessage API for Go?
Yes, over plain HTTP. Flow's docs include a standard-library Go example and say a Go SDK is not published yet.
Do I need a Go SDK to use Flow?
No. The API works from any language over HTTP, with a bearer key and JSON bodies. The TypeScript and Python SDKs wrap the same calls.
How do I avoid duplicate replies in Go?
Send an Idempotency-Key header with each POST. Using the inbound event's ID as the key means a retry returns the first answer and does not send twice.
How do I verify a webhook signature in Go?
Compute HMAC-SHA256 over the string t., the timestamp, a dot, then the raw body, keyed with your endpoint secret. Compare it in constant time with the v1 value, and reject timestamps more than five minutes from your clock.
How do I pin the API version?
Send a Flow-Version header. Flow's Go example sends Flow-Version 2026-11-01 on each request.
Can Go use the live stream?
Yes. Any WebSocket client that can set an Authorization header can read wss://api.flow.engineer/v1/stream. The Go example in the quickstart polls the event log instead, which is simpler.
Keep reading
All posts: the Flow blog. The Flow docs have the reference.
From reading to building
Your agent, on iMessage and everywhere else.
Flow gives your agent an iMessage line, and the same code answers on WhatsApp and Telegram and works as an MCP server and CLI. See how it works on iMessage.
Build on iMessage with Flow