Help us shape the future of Go! Take the 2026 Go Developer Survey

sendimessage

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 17 Imported by: 0

README

SendiMessage SDK for Go

Go Reference Version

The official Go client for the SendiMessage API — send and receive iMessage and SMS programmatically. Standard library only, no external dependencies.

Server-side only. The API key pair grants full messaging access to your account — treat it like any other production secret.

Contents

Requirements

  • Go ≥ 1.21
  • A SendiMessage account with an API key pair

Installation

go get github.com/sendimessage/sendimessage-go

Authentication

Create a key pair in the account portal under API credentials. You get a key_id and a secret — the secret is shown once, store it securely (e.g. in your secret manager or environment variables). Every request the SDK makes carries them as the X-API-Key / X-API-Secret headers.

client := sendimessage.New(os.Getenv("SENDIMESSAGE_KEY"), os.Getenv("SENDIMESSAGE_SECRET"))

You can verify a key pair at any time with GET /me (curl example in the account portal).

Quick start

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	sendimessage "github.com/sendimessage/sendimessage-go"
)

func main() {
	client := sendimessage.New(os.Getenv("SENDIMESSAGE_KEY"), os.Getenv("SENDIMESSAGE_SECRET"))

	msg, err := client.SendMessage(context.Background(), sendimessage.SendMessageParams{
		Number:  "+15551234567",
		Content: "Hello from SendiMessage!",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(msg.MessageHandle, msg.Status) // "…" "QUEUED"
}

The API delivers over iMessage first and falls back to SMS when the recipient is not reachable over iMessage.

Usage

Sending messages
msg, err := client.SendMessage(ctx, sendimessage.SendMessageParams{
	Number:  "+15551234567", // E.164 phone number, or an email for iMessage
	Content: "Your order has shipped 🎉",
})

If your account has more than one line (sending number), route via a specific one with LineHandle:

lines, _ := client.ListLines(ctx) // {"lines": [{"line_handle": ..., "number": ...}, ...]}
lineHandle := lines["lines"].([]any)[0].(map[string]any)["line_handle"].(string)
msg, err := client.SendMessage(ctx, sendimessage.SendMessageParams{
	Number:     "+15551234567",
	Content:    "Hi!",
	LineHandle: "LI…", // from ListLines
})
Sending media

Upload the file first — UploadMedia returns a public URL you pass as MediaURL. Images, video, audio and PDF are supported, up to 50 MiB.

url, err := client.UploadMedia(ctx, "./photo.jpg")            // path
// or: client.UploadMediaReader(ctx, reader, "photo.jpg")     // any io.Reader

msg, err := client.SendMessage(ctx, sendimessage.SendMessageParams{
	Number:   "+15551234567",
	MediaURL: url,
})

Content is optional when MediaURL is set; provide both to send a caption with the attachment. Any public http(s) URL also works as MediaURL.

Scheduling and status callbacks
msg, err := client.SendMessage(ctx, sendimessage.SendMessageParams{
	Number:         "+15551234567",
	Content:        "Reminder: appointment at 3pm",
	ScheduledAt:    "2026-08-10T14:30:00Z",                // ISO 8601, deliver later
	StatusCallback: "https://example.com/hooks/status",    // POSTed the final status
})
Message status and history

Every send returns a MessageHandle. Poll it — or better, use a webhook / StatusCallback:

status, err := client.GetStatus(ctx, msg.MessageHandle)
fmt.Println(status.Status) // QUEUED | SENT | ERROR

History is cursor-paginated: pass NextBefore from one page as Before on the next. NextBefore is empty on the last page.

params := sendimessage.ListMessagesParams{Number: "+15551234567", Limit: 50}
for {
	page, err := client.ListMessages(ctx, params)
	if err != nil {
		log.Fatal(err)
	}
	for _, m := range page.Messages {
		dir := "←"
		if m.IsOutbound {
			dir = "→"
		}
		fmt.Println(m.DateSent, dir, m.Content)
	}
	if page.NextBefore == "" {
		break
	}
	params.Before = page.NextBefore
}

Filters: Number, Direction ("in" / "out"), LineHandle, ConversationHandle, Service, Since, Until, Limit.

one, err := client.GetMessage(ctx, handle) // single history message
iMessage lookup

Check whether a number is reachable over iMessage before sending:

result, err := client.Lookup(ctx, "+15551234567")
fmt.Println(result.Service) // "iMessage" | "SMS"
fmt.Println(result.Cached)  // true when answered from the capability cache

Lookups are answered by a live Apple device, so the API is cache-first: a fresh cached answer returns immediately, otherwise the question is queued and the call waits briefly for a device. If no answer arrives in time you get a pending response — retry shortly; a 503 means no device is currently online for your account.

Contacts and opt-out

Contacts are your address book: names shown in conversations, the detected service (iMessage/SMS) and the opt-out flag. Contact endpoints use sendimessage.Object (map[string]any) for flexible properties.

_, err = client.CreateContact(ctx, sendimessage.Object{
	"number": "+15551234567", "first_name": "Jane", "last_name": "Doe",
})
_, err = client.UpdateContact(ctx, "+15551234567", sendimessage.Object{"company_name": "Acme Inc."})
contact, err := client.GetContact(ctx, "+15551234567") // number or contact_handle
all, err := client.ListContacts(ctx, url.Values{"limit": {"100"}})
_, err = client.DeleteContact(ctx, "+15551234567")
_, err = client.RecheckContact(ctx, "+15551234567") // re-run the iMessage/SMS check now

Opt-out is enforced on every send path — a send to an opted-out number fails with HTTP 422. Inbound STOP / UNSUBSCRIBE / CANCEL / END / QUIT opts the sender out automatically; START / UNSTOP / YES opts back in. You can also manage it explicitly:

_, err = client.SetOptOut(ctx, "+15551234567", true)  // opt out
_, err = client.SetOptOut(ctx, "+15551234567", false) // opt back in
Lines and call forwarding

A line is a sending identity (phone number or iMessage address) on your account.

lines, err := client.ListLines(ctx)
lineHandle := lines["lines"].([]any)[0].(map[string]any)["line_handle"].(string)
_, err = client.UpdateLine(ctx, lineHandle, sendimessage.Object{
	"label": "Support line", "is_active": true,
})

Calls to your line's number can be forwarded to any number you choose. Forwarding is configured by the SendiMessage operator, so changes are asynchronous — the API answers 202 and the request shows as pending until fulfilled:

number := "+15559876543"
_, err = client.SetCallForwarding(ctx, lineHandle, &number) // request forwarding
_, err = client.SetCallForwarding(ctx, lineHandle, nil)     // request turning it off
state, err := client.GetCallForwarding(ctx, lineHandle)     // current + pending
Conversations

Conversations group the message history per contact:

convos, err := client.ListConversations(ctx, url.Values{"per_page": {"20"}})
msgs, err := client.ListConversationMessages(ctx, conversationHandle, url.Values{"per_page": {"50"}})
_, err = client.MarkConversationRead(ctx, conversationHandle)
Webhooks

Get pushed events instead of polling. Events:

Event Fired when
receive An inbound message arrives
outbound An outbound message reaches its final status (sent/failed)
line_blocked A line was blocked by Apple (sends answer 422 until unblocked)
line_unblocked The block was lifted
_, err = client.CreateWebhook(ctx, sendimessage.WebhookParams{
	URL:    "https://example.com/hooks/sendimessage",
	Events: []string{"receive", "outbound"},   // default: every event
	Secret: os.Getenv("WEBHOOK_SECRET"),       // enables the signature header
})

hooks, err := client.ListWebhooks(ctx)
_, err = client.ReplaceWebhooks(ctx, []sendimessage.WebhookParams{...}) // swap the whole set
_, err = client.DeleteWebhook(ctx, "https://example.com/hooks/sendimessage")

When the webhook has a Secret, every delivery carries an X-SMSBridge-Signature header — the hex HMAC-SHA256 of the raw request body. Always verify it (constant-time comparison built in):

http.HandleFunc("/hooks/sendimessage", func(w http.ResponseWriter, r *http.Request) {
	// The raw body is required — re-serialized JSON differs byte-for-byte.
	rawBody, err := io.ReadAll(r.Body)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	ok := sendimessage.VerifyWebhookSignature(
		os.Getenv("WEBHOOK_SECRET"),
		rawBody,
		r.Header.Get("X-SMSBridge-Signature"),
	)
	if !ok {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	var event map[string]any
	_ = json.Unmarshal(rawBody, &event)
	// ... handle event
	w.WriteHeader(http.StatusOK)
})

Error handling

Every non-2xx response returns an *APIError with Status (HTTP code), Message (API error text) and Body (the raw response).

msg, err := client.SendMessage(ctx, sendimessage.SendMessageParams{
	Number: "+15551234567", Content: "hi",
})
if err != nil {
	var apiErr *sendimessage.APIError
	if errors.As(err, &apiErr) {
		log.Printf("API error %d: %s", apiErr.Status, apiErr.Message)
	} else {
		log.Printf("transport error: %v", err) // network error etc.
	}
}

Common statuses:

Status Meaning
401 Bad or missing key pair
404 Unknown handle (message, contact, line, conversation)
422 Validation failed — including sends to an opted-out contact or via a blocked line
429 Rate limited — back off and retry
503 Lookup: no device online for your account

Configuration

client := sendimessage.New(key, secret,
	sendimessage.WithBaseURL("https://api.sendimessage.com/v1"), // default; override for testing
	sendimessage.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}),
)

Every method takes a context.Context — use it for per-call timeouts and cancellation.

API reference

Method Endpoint
SendMessage(ctx, params) POST /send-message
GetStatus(ctx, handle) GET /status
ListMessages(ctx, params) GET /v2/messages
GetMessage(ctx, handle) GET /v2/messages/{handle}
UploadMedia(ctx, path) / UploadMediaReader(ctx, r, filename) POST /media
Lookup(ctx, number) GET /lookup
ListContacts(ctx, query) GET /contacts
CreateContact(ctx, props) POST /contacts
GetContact(ctx, number) GET /contacts/{number}
UpdateContact(ctx, number, props) PUT /contacts/{number}
DeleteContact(ctx, number) DELETE /contacts/{number}
SetOptOut(ctx, number, optedOut) POST /contacts/opt-out
RecheckContact(ctx, number) POST /contacts/{number}/lookup
ListLines(ctx) GET /lines
UpdateLine(ctx, handle, props) PUT /lines/{handle}
GetCallForwarding(ctx, handle) GET /lines/{handle}/call-forwarding
SetCallForwarding(ctx, handle, number) PUT /lines/{handle}/call-forwarding
ListConversations(ctx, query) GET /conversations
ListConversationMessages(ctx, handle, query) GET /conversations/{handle}/messages
MarkConversationRead(ctx, handle) POST /conversations/{handle}/read
ListWebhooks(ctx) GET /account/webhooks
CreateWebhook(ctx, params) POST /account/webhooks
ReplaceWebhooks(ctx, webhooks) PUT /account/webhooks
DeleteWebhook(ctx, url) DELETE /account/webhooks

Full godoc: pkg.go.dev/github.com/sendimessage/sendimessage-go. Full HTTP-level reference: the Postman collection.

License

MIT

Forcing SMS

By default the Mac tries iMessage and falls back to SMS when Apple reports the number has no iMessage. Set Service: "sms" to send a plain text message straight away (or "imessage" to insist on the default path):

msg, err := client.SendMessage(ctx, sendimessage.SendMessageParams{Number: "+15551234567", Content: "Your code is 1234", Service: "sms"})

client.Me(ctx) returns the user + company behind the key pair — the cheapest way to verify credentials.

Documentation

Overview

Package sendimessage is the SendiMessage SDK — send iMessage/SMS through the SendiMessage API. Server-side use: the key pair grants full messaging access.

client := sendimessage.New(apiKey, apiSecret)
msg, err := client.SendMessage(ctx, sendimessage.SendMessageParams{
	Number:  "+15551234567",
	Content: "Hello!",
})

Index

Constants

View Source
const DefaultBaseURL = "https://api.sendimessage.com/v1"
View Source
const Version = "0.1.1"

Version of this client, sent as the User-Agent.

Variables

This section is empty.

Functions

func VerifyWebhookSignature

func VerifyWebhookSignature(secret string, rawBody []byte, signature string) bool

VerifyWebhookSignature checks the X-SMSBridge-Signature header of an incoming webhook: hex HMAC-SHA256 of the raw request body with the webhook's secret. Pass the body exactly as received.

Types

type APIError

type APIError struct {
	Status  int
	Message string
	Body    []byte
}

APIError is returned for any non-2xx API response.

func (*APIError) Error

func (e *APIError) Error() string

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to the SendiMessage public API (X-API-Key / X-API-Secret).

func New

func New(apiKey, apiSecret string, opts ...Option) *Client

New creates a Client with the key pair from the account portal.

func (*Client) CreateContact

func (c *Client) CreateContact(ctx context.Context, props Object) (Object, error)

func (*Client) CreateWebhook

func (c *Client) CreateWebhook(ctx context.Context, params WebhookParams) (Object, error)

func (*Client) DeleteContact

func (c *Client) DeleteContact(ctx context.Context, number string) (Object, error)

func (*Client) DeleteWebhook

func (c *Client) DeleteWebhook(ctx context.Context, webhookURL string) (Object, error)

func (*Client) GetCallForwarding

func (c *Client) GetCallForwarding(ctx context.Context, lineHandle string) (Object, error)

func (*Client) GetContact

func (c *Client) GetContact(ctx context.Context, number string) (Object, error)

GetContact fetches a contact; number is the E.164 number or contact_handle.

func (*Client) GetMessage

func (c *Client) GetMessage(ctx context.Context, messageHandle string) (*HistoryMessage, error)

GetMessage returns one history message by its handle.

func (*Client) GetStatus

func (c *Client) GetStatus(ctx context.Context, messageHandle string) (*OutboundMessage, error)

GetStatus reports the current status of a message by its handle.

func (*Client) ListContacts

func (c *Client) ListContacts(ctx context.Context, query url.Values) (Object, error)

func (*Client) ListConversationMessages

func (c *Client) ListConversationMessages(ctx context.Context, conversationHandle string, query url.Values) (Object, error)

func (*Client) ListConversations

func (c *Client) ListConversations(ctx context.Context, query url.Values) (Object, error)

func (*Client) ListLines

func (c *Client) ListLines(ctx context.Context) (Object, error)

func (*Client) ListMessages

func (c *Client) ListMessages(ctx context.Context, params ListMessagesParams) (*MessageList, error)

ListMessages returns one page of message history.

func (*Client) ListWebhooks

func (c *Client) ListWebhooks(ctx context.Context) (Object, error)

func (*Client) Lookup

func (c *Client) Lookup(ctx context.Context, number string) (*LookupResult, error)

Lookup answers whether a number has iMessage.

func (*Client) MarkConversationRead

func (c *Client) MarkConversationRead(ctx context.Context, conversationHandle string) (Object, error)

func (*Client) Me added in v0.1.1

func (c *Client) Me(ctx context.Context) (Object, error)

Me returns the user + company behind these credentials (GET /me).

func (*Client) RecheckContact

func (c *Client) RecheckContact(ctx context.Context, number string) (Object, error)

RecheckContact re-runs the iMessage/SMS service check right now.

func (*Client) ReplaceWebhooks

func (c *Client) ReplaceWebhooks(ctx context.Context, webhooks []WebhookParams) (Object, error)

ReplaceWebhooks swaps the whole webhook set in one call.

func (*Client) SendMessage

func (c *Client) SendMessage(ctx context.Context, params SendMessageParams) (*OutboundMessage, error)

SendMessage queues a message; the result carries message_handle and status QUEUED.

func (*Client) SetCallForwarding

func (c *Client) SetCallForwarding(ctx context.Context, lineHandle string, forwardingNumber *string) (Object, error)

SetCallForwarding queues a forwarding change for the operator (202); nil turns forwarding off.

func (*Client) SetOptOut

func (c *Client) SetOptOut(ctx context.Context, number string, optedOut bool) (Object, error)

SetOptOut opts a number out of (or back into) all messaging.

func (*Client) UpdateContact

func (c *Client) UpdateContact(ctx context.Context, number string, props Object) (Object, error)

func (*Client) UpdateLine

func (c *Client) UpdateLine(ctx context.Context, lineHandle string, props Object) (Object, error)

UpdateLine changes a line's label / is_active by its line_handle. Pass a props map with "label" and/or "is_active".

func (*Client) UploadMedia

func (c *Client) UploadMedia(ctx context.Context, path string) (string, error)

UploadMedia uploads a file (image/video/audio/PDF, max 50 MiB) and returns the public URL to pass as MediaURL in SendMessageParams.

func (*Client) UploadMediaReader

func (c *Client) UploadMediaReader(ctx context.Context, r io.Reader, filename string) (string, error)

UploadMediaReader uploads media from any reader under the given filename.

type HistoryMessage

type HistoryMessage struct {
	MessageHandle      string `json:"message_handle"`
	Number             string `json:"number"`
	Content            string `json:"content"`
	MediaURL           string `json:"media_url"`
	Service            string `json:"service"`
	IsOutbound         bool   `json:"is_outbound"`
	Status             string `json:"status"` // DELIVERED | SENT | RECEIVED
	DateSent           string `json:"date_sent"`
	ConversationHandle string `json:"conversation_handle"`
}

HistoryMessage is one row of the message history (both directions).

type ListMessagesParams

type ListMessagesParams struct {
	Number             string
	Direction          string // "in" | "out"
	LineHandle         string
	ConversationHandle string
	Service            string
	Since              string
	Until              string
	Limit              int
	Before             string
}

ListMessagesParams filters the message history. Before is the cursor: pass NextBefore from the previous page.

type LookupResult

type LookupResult struct {
	Number     string `json:"number"`
	Service    string `json:"service"` // iMessage | SMS; empty while pending
	Cached     bool   `json:"cached"`
	CheckedAt  string `json:"checked_at"`
	Status     string `json:"status,omitempty"`     // "pending" on a 202 — retry shortly
	Unresolved bool   `json:"unresolved,omitempty"` // Apple gave no verdict; "SMS" is a safe default, not knowledge
}

LookupResult answers "does this number have iMessage?".

type MessageList

type MessageList struct {
	Messages   []HistoryMessage `json:"messages"`
	NextBefore string           `json:"next_before"`
}

MessageList is one page of history plus the cursor for the next one.

type Object

type Object = map[string]any

Generic JSON object for endpoints whose shapes the SDK leaves untyped.

type Option

type Option func(*Client)

Option customizes the Client.

func WithBaseURL

func WithBaseURL(u string) Option

WithBaseURL overrides the API base URL (default DefaultBaseURL).

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient supplies a custom *http.Client.

type OutboundMessage

type OutboundMessage struct {
	Status           string          `json:"status"` // QUEUED | SENT | ERROR
	MessageHandle    string          `json:"message_handle"`
	From             string          `json:"from"`
	To               json.RawMessage `json:"to"` // one number, or the participant list for group sends
	GroupID          string          `json:"group_id"`
	Content          string          `json:"content"`
	MediaURL         string          `json:"media_url"`
	Service          string          `json:"service"`
	RequestedService string          `json:"requested_service"` // "SMS" when the daemon was told to skip iMessage
	IsOutbound       bool            `json:"is_outbound"`
	ErrorMessage     string          `json:"error_message"`
	DateCreated      string          `json:"date_created"`
	DateUpdated      string          `json:"date_updated"`
	DateSent         string          `json:"date_sent"`
}

OutboundMessage is the public shape of a queued/sent message.

type SendMessageParams

type SendMessageParams struct {
	Number         string `json:"number"`
	Content        string `json:"content,omitempty"`
	MediaURL       string `json:"media_url,omitempty"`       // from UploadMedia, or any public URL
	LineHandle     string `json:"line_handle,omitempty"`     // route via a specific line (see ListLines)
	StatusCallback string `json:"status_callback,omitempty"` // POSTed the final status
	ScheduledAt    string `json:"scheduled_at,omitempty"`    // ISO 8601 — deliver later
	Service        string `json:"service,omitempty"`         // "sms" = plain text, no iMessage attempt; "imessage" = default path
}

SendMessageParams describes one outbound message. Content is optional when MediaURL is set.

type WebhookParams

type WebhookParams struct {
	URL    string   `json:"url"`
	Events []string `json:"events,omitempty"` // receive | outbound | line_blocked | line_unblocked
	Secret string   `json:"secret,omitempty"`
}

WebhookParams configures one webhook. Events defaults to every event; Secret enables the X-SMSBridge-Signature HMAC on deliveries.

Directories

Path Synopsis
Smoke test for the Go SDK.
Smoke test for the Go SDK.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL