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