omnistream

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 17 Imported by: 0

README

omnistream-go

Official Go SDK for the OmniStream omnichannel CRM API. The client handles auth, retries, pagination, idempotency and webhook verification; typed helpers cover five resources, and everything else is reachable through a generic escape hatch.

Install

go get github.com/Cepat-Kilat-Teknologi/omnistream-go

Requires Go 1.23 or newer. Zero third-party dependencies.

Quick start

package main

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

	omnistream "github.com/Cepat-Kilat-Teknologi/omnistream-go"
)

func main() {
	client, err := omnistream.NewClient(
		os.Getenv("OMNISTREAM_API_KEY"),                         // create in Developer → API Keys
		omnistream.WithBaseURL("https://your-omnistream-host"), // default http://localhost:3000
	)
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()

	// Typed resource helpers
	open, err := client.Conversations.List(ctx, &omnistream.ConversationListParams{
		Status: omnistream.ConversationOpen,
	})
	if err != nil {
		log.Fatal(err)
	}
	if len(open) == 0 {
		log.Fatal("no open conversations")
	}

	msg, err := client.Conversations.SendMessage(ctx, open[0].ID, omnistream.TextMessage("Hi! How can I help?"))
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(msg.ID, msg.Status)
}

The SDK covers five resources with typed helpers — Conversations, Contacts, Messages, Templates, APIKeys. Everything else — the API's other ~218 endpoints — goes through Request (any method, including PATCH), its Get/Post/Delete shorthands, or Do (see Escape hatch). This is not full API coverage.

Features

Auth

Every request sends your key as X-API-Key. Lock the key down with an IP allow list when you create it:

created, err := client.APIKeys.Create(ctx, omnistream.CreateAPIKeyRequest{
    Name:       "billing-service",
    KeyType:    "rest",
    AllowedIPs: []string{"203.0.113.10"},
})
if err != nil {
    log.Fatal(err)
}
// created.PlaintextKey is shown exactly once — store it now, it is not
// recoverable later. created.Key.KeyPrefix identifies it afterwards.
fmt.Println(created.PlaintextKey)
Retries & rate limits

Failed requests are retried automatically with exponential backoff and full jitter. The retry rule depends on the method:

  • GET retries on network errors and 5xx responses.
  • Any other method (POST, DELETE, ...) retries only if the request carries an idempotency key — otherwise a retry could duplicate the effect.
  • 429 Too Many Requests always retries, regardless of method, and honours the Retry-After header when present.
client, err := omnistream.NewClient(apiKey,
    omnistream.WithMaxRetries(3),
    omnistream.WithTimeout(15*time.Second),
)

WithTimeout bounds a single HTTP attempt, not the whole call — a retried request gets a fresh timeout per attempt. To bound the total time across all attempts, use a context deadline:

ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
client.Conversations.List(ctx, nil)

WithHTTPClient swaps in your own *http.Client for a custom transport, proxy, or tracing. A Timeout set on that client stays in force — net/http enforces it per attempt alongside WithTimeout, whichever is shorter — and its expiry still reports NetworkError.TimedOut == true.

Idempotency

Pass an idempotency key so a POST can be safely retried on transient failures:

key, err := omnistream.NewIdempotencyKey()
if err != nil {
    log.Fatal(err)
}

msg, err := client.Conversations.SendMessage(ctx, convID,
    omnistream.TextMessage("hi"),
    omnistream.WithIdempotencyKey(key),
)

WithRequestRetries overrides the client's retry count for a single call.

Pagination

Paginate returns an iter.Seq2[T, error] that walks a page-numbered list endpoint lazily, fetching each page only as the loop reaches it. List methods still return one page; All methods wrap Paginate for the resources that support it:

for contact, err := range client.Contacts.All(ctx, &omnistream.ContactListParams{Search: "acme"}) {
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(contact.ID)
}

Iteration stops after the first error, when the context is done, or when the loop body breaks. The page size is clamped to 100, the largest page the server honours — asking for more would come back clamped anyway, and the paginator would mistake that short page for the end of the data.

Contacts are the one listing the API wraps in a pagination envelope. Contacts.List unwraps it, so it returns []Contact like every other List; Contacts.ListPage hands back the envelope when you need the totals:

page, err := client.Contacts.ListPage(ctx, &omnistream.ContactListParams{PerPage: 20})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%d contacts on page %d of %d\n", len(page.Data), page.Page, page.TotalPages)

Messages.Search runs a full-text search across conversations and returns one page of hits with the totals:

page, err := client.Messages.Search(ctx, &omnistream.MessageSearchParams{
    Query:     "invoice",
    Channel:   "whatsapp",
    Direction: omnistream.DirectionInbound,
    From:      "2026-08-01T00:00:00Z",
    PerPage:   50, // server clamps to 1..50
})
if err != nil {
    log.Fatal(err)
}
for _, hit := range page.Results {
    fmt.Println(hit.ID, hit.Snippet) // Snippet marks the match with <mark> tags
}
fmt.Printf("page %d of %d, %d hits total\n", page.Page, page.TotalPages, page.Total)

A search hit is a MessageSearchResult, not a Message: it adds a highlighted Snippet and denormalised contact fields, and carries no delivery status, provider ID, or failure detail. The Snippet is an HTML fragment built from customer-supplied text — escape everything outside the <mark> tags before rendering it.

Typed errors
_, err := client.Conversations.Get(ctx, "does-not-exist")
if err != nil {
    var apiErr *omnistream.APIError
    if errors.As(err, &apiErr) && omnistream.IsNotFound(err) {
        // 404
    }
    var netErr *omnistream.NetworkError
    if errors.As(err, &netErr) && netErr.TimedOut {
        // request timed out
    }
}

APIError exposes StatusCode, Code, Message, and Body. Status predicates: IsAuthError (401), IsForbidden (403), IsNotFound (404), IsValidationError (422), IsRateLimited (429), IsServerError (5xx). NetworkError reports a request that never produced an HTTP response — a dial failure, a timeout, or a cancelled context — and unwraps to the underlying cause.

Webhook verification

Verify inbound webhooks signed by the gateway (X-Omnistream-Signature, HMAC-SHA256 hex) before parsing the body — an unverified body is untrusted input. A webhook configured without a secret is delivered unsigned, and Verify rejects that case rather than accepting it.

import "github.com/Cepat-Kilat-Teknologi/omnistream-go/webhook"

// rawBody must be the exact bytes received — verify before json.Unmarshal.
ok := webhook.Verify(rawBody, r.Header.Get(webhook.SignatureHeader), secret)

// During a secret rotation, accept the current OR previous signature:
okRotating := webhook.VerifyWithRotation(rawBody, secret,
    r.Header.Get(webhook.SignatureHeader),
    r.Header.Get(webhook.SignaturePreviousHeader),
)

Verify takes the header value exactly as delivered, sha256= prefix included, and rejects a bare hex digest. If you are porting from the TypeScript SDK, note that its equivalent expects the prefix already stripped — pass the raw header here and do not pre-process it.

For a full endpoint, use webhook.Handler, which verifies, decodes, and dispatches to typed callbacks:

h, err := webhook.NewHandler(webhookSecret)
if err != nil {
    log.Fatal(err) // an empty secret is rejected here, not at delivery time
}

h.On(webhook.EventMessageReceived, func(ctx context.Context, e webhook.Event) error {
    if e.Data.Message == nil {
        return nil
    }
    log.Printf("inbound on %s: %s", e.Data.ConversationID, e.Data.Message.ContentPreview)
    return nil
})

http.Handle("/webhooks/omnistream", h)

On registers a callback per EventType (EventMessageReceived, EventMessageSent, EventMessageStatus, EventConversationCreated, EventConversationResolved, EventConversationAssigned); OnAny catches types with no specific handler, including ones this SDK version does not know about. An unhandled event still gets acknowledged with 204, not rejected, so a new event type never trips the dispatcher's failure-based suspension. WithMaxBodyBytes caps the body size accepted by NewHandler (default 1 MiB).

NewHandler returns an error for an empty secret rather than a handler that rejects every delivery: Verify fails closed without a secret, and the dispatcher suspends a webhook after ten consecutive failures — so a typo in an environment variable would otherwise be a silent, self-inflicted outage.

See examples/receive_webhook for a runnable server and examples/send_message for a runnable client call.

Escape hatch

Every one of the API's other endpoints is reachable through the generic verbs. Define a struct matching the response shape you expect:

// Campaign is a struct you define to match the endpoint's response shape;
// the SDK has no typed helper for it.
type Campaign struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

var campaigns []Campaign
err := client.Get(ctx, "/api/campaigns", url.Values{"page": {"1"}}, &campaigns)

err = client.Delete(ctx, "/api/api-keys/"+keyID, nil)

Get, Post, and Delete are wrappers over Request, which takes any method — reach the API's many PATCH endpoints with it:

var updated Campaign
err := client.Request(ctx, http.MethodPatch, "/api/campaigns/"+id,
    nil,                                   // query
    map[string]string{"name": "renewals"}, // body
    &updated,                              // decoded into
    omnistream.WithIdempotencyKey(key),
)

Request runs the full pipeline — auth, retries, idempotency, typed errors. Like the named verbs, it retries a non-GET only when the call carries an idempotency key.

Do sends a caller-built *http.Request with auth attached and returns the raw *http.Response, for streaming or non-JSON endpoints. It needs an absolute URL, which BaseURL supplies:

req, err := http.NewRequest(http.MethodGet, client.BaseURL()+"/api/media/"+mediaID, nil)
if err != nil {
    log.Fatal(err)
}
resp, err := client.Do(ctx, req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

Unlike Get, Post, Delete, and Request, Do never retries, since the request body may not be replayable.

Examples

Development

make check     # tests with -race, vet, staticcheck, and the coverage gate
Smoke test

The unit tests assert against response bodies the tests themselves write, so they cannot see a wrong wire contract — they will happily agree with a mistaken idea of what the API returns. make smoke is the only check that reads a body the SDK did not write, and it asserts on decoded field values rather than on err == nil.

OMNISTREAM_API_KEY=os_... OMNISTREAM_BASE_URL=https://your-host make smoke

It exercises every typed service read-only. Pass SEND=1 to additionally deliver one real message to the first open conversation — that reaches a real customer, so it is opt-in.

Run it before tagging a release. Three separate response-shape bugs reached the eve of v0.1.0 despite a full review pass; this test caught the last of them on its first run.

Documentation

Overview

Package omnistream is the official Go client for the OmniStream omnichannel CRM REST API.

Index

Constants

View Source
const (
	// DefaultBaseURL is the API gateway address used when none is supplied.
	DefaultBaseURL = "http://localhost:3000"
	// DefaultTimeout bounds a single HTTP attempt.
	DefaultTimeout = 30 * time.Second
	// DefaultMaxRetries is the number of retries after the first attempt.
	DefaultMaxRetries = 2
)

Client defaults.

View Source
const Version = "0.1.0"

Version is the SDK version, reported in the User-Agent header.

Variables

This section is empty.

Functions

func IsAuthError

func IsAuthError(err error) bool

IsAuthError reports whether err is a 401 Unauthorized response.

func IsForbidden

func IsForbidden(err error) bool

IsForbidden reports whether err is a 403 Forbidden response.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is a 404 Not Found response.

func IsRateLimited

func IsRateLimited(err error) bool

IsRateLimited reports whether err is a 429 Too Many Requests response.

func IsServerError

func IsServerError(err error) bool

IsServerError reports whether err is a 5xx response.

func IsValidationError

func IsValidationError(err error) bool

IsValidationError reports whether err is a 422 Unprocessable Entity response.

func NewIdempotencyKey

func NewIdempotencyKey() (string, error)

NewIdempotencyKey returns a random RFC 4122 version 4 UUID suitable for the Idempotency-Key header. It fails only if the system entropy source does.

func Paginate

func Paginate[T any](ctx context.Context, c *Client, path string, query url.Values, perPage int) iter.Seq2[T, error]

Paginate walks a page-numbered endpoint that returns a JSON array, yielding one item at a time and fetching pages lazily. Iteration stops after a page returns fewer than perPage items, after the first error, when the context is done, or when the caller breaks out of the loop.

perPage is clamped to 100, the largest page the server honours. A larger request would come back clamped to 100 anyway, and that short page would be mistaken for the end of the data. A non-positive perPage uses 50.

The error value is non-nil at most once, on the final iteration.

Types

type APIError

type APIError struct {
	// StatusCode is the HTTP status code of the response.
	StatusCode int
	// Code is the application error code from the {"error":{"code":N}} body
	// shape. Zero when the response did not carry one.
	Code int
	// Message is a human-readable description of the failure.
	Message string
	// Body is the raw response body, preserved because not every endpoint
	// honours the documented error shape.
	Body []byte
}

APIError is returned when the API responds with a non-2xx status.

func (*APIError) Error

func (e *APIError) Error() string

Error implements the error interface.

type APIKey

type APIKey struct {
	ID      string `json:"id"`
	AgentID string `json:"agent_id"`
	Name    string `json:"name"`
	// KeyType is "rest" or "webhook_signing".
	KeyType string `json:"key_type"`
	// KeyPrefix is the non-secret leading portion of the plaintext key.
	KeyPrefix string          `json:"key_prefix"`
	Scopes    json.RawMessage `json:"scopes"`
	// AllowedIPs restricts the source IPs accepted for a "rest" key. Empty
	// means any IP is accepted.
	AllowedIPs []string   `json:"allowed_ips,omitempty"`
	LastUsedAt *time.Time `json:"last_used_at,omitempty"`
	ExpiresAt  *time.Time `json:"expires_at,omitempty"`
	IsActive   bool       `json:"is_active"`
	CreatedAt  time.Time  `json:"created_at"`
	RevokedAt  *time.Time `json:"revoked_at,omitempty"`
}

APIKey is an API credential belonging to an agent.

type APIKeyCreateResponse

type APIKeyCreateResponse struct {
	Key APIKey `json:"key"`
	// PlaintextKey is shown only at creation time. Store it securely.
	PlaintextKey string `json:"plaintext_key"`
}

APIKeyCreateResponse carries the created key plus its plaintext value, which the API returns exactly once.

type APIKeysService

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

APIKeysService accesses the API key endpoints. Creating a key requires the developer.access permission.

func (*APIKeysService) Create

Create issues a new API key. The plaintext key in the response is shown exactly once — store it before discarding the response.

func (*APIKeysService) List

func (s *APIKeysService) List(ctx context.Context) ([]APIKey, error)

List returns the calling agent's API keys.

func (*APIKeysService) Revoke

func (s *APIKeysService) Revoke(ctx context.Context, id string, opts ...RequestOption) error

Revoke permanently disables an API key.

Revoking is idempotent server-side, but a WithIdempotencyKey option still makes the DELETE eligible for retry after a transient failure, which it otherwise is not.

type Assignee

type Assignee struct {
	// AgentID is set when this assignee is a human agent.
	AgentID *string `json:"agent_id,omitempty"`
	// AIAgentID is set when this assignee is an AI agent.
	AIAgentID *string `json:"ai_agent_id,omitempty"`
	// Name is the display name of the human or AI agent.
	Name *string `json:"name,omitempty"`
	// Kind discriminates the two: "human" or "ai".
	Kind string `json:"kind"`
}

Assignee is a human agent or AI agent attached to a conversation.

type Client

type Client struct {

	// Conversations accesses the conversation endpoints.
	Conversations *ConversationsService
	// Contacts accesses the contact endpoints.
	Contacts *ContactsService
	// Messages accesses cross-conversation message endpoints.
	Messages *MessagesService
	// Templates accesses the WhatsApp template endpoints.
	Templates *TemplatesService
	// APIKeys accesses the API key endpoints.
	APIKeys *APIKeysService
	// contains filtered or unexported fields
}

Client is an OmniStream API client. It is safe for concurrent use.

func NewClient

func NewClient(apiKey string, opts ...Option) (*Client, error)

NewClient builds a client authenticated with a REST API key. Create keys in the OmniStream dashboard under Developer, API Keys.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the API gateway address requests are sent to, without a trailing slash. Use it to build the absolute URL that Do requires.

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, path string, out any, opts ...RequestOption) error

Delete performs a DELETE request and decodes any response into out.

func (*Client) Do

func (c *Client) Do(ctx context.Context, req *http.Request) (*http.Response, error)

Do sends a caller-built request with authentication attached and returns the raw response, for streaming or non-JSON endpoints. The caller must close the response body. Do never retries: the request body may not be replayable.

func (*Client) Get

func (c *Client) Get(ctx context.Context, path string, query url.Values, out any, opts ...RequestOption) error

Get performs a GET request and decodes a JSON response into out. Pass a nil out to discard the body.

func (*Client) Post

func (c *Client) Post(ctx context.Context, path string, body, out any, opts ...RequestOption) error

Post performs a POST request with a JSON body and decodes the response into out. Supply WithIdempotencyKey to make the request safe to retry.

func (*Client) Request

func (c *Client) Request(ctx context.Context, method, path string, query url.Values, body, out any, opts ...RequestOption) error

Request performs an arbitrary HTTP request against the API and decodes a JSON response into out. It is the general escape hatch for the endpoints the typed services do not cover, including the API's many PATCH routes; Get, Post, and Delete are thin wrappers over it.

path is appended to the client's base URL and must begin with a slash. Pass a nil query, a nil body, or a nil out to omit any of them. Retries follow the same rules as the named verbs: methods other than GET are retried only when the call carries WithIdempotencyKey.

type Contact

type Contact struct {
	ID          string  `json:"id"`
	PhoneNumber *string `json:"phone_number,omitempty"`
	Name        *string `json:"name,omitempty"`
	Email       *string `json:"email,omitempty"`
	// ChannelSource is "whatsapp", "instagram", or "email".
	ChannelSource string    `json:"channel_source"`
	Tags          []string  `json:"tags"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
}

Contact is a customer record.

type ContactListParams

type ContactListParams struct {
	// Search matches name, phone, or email, case-insensitively.
	Search string
	// Tag filters to contacts carrying this tag.
	Tag string
	// Page is 1-based. Zero means the server default.
	Page int
	// PerPage is capped at 100 by the server. Zero means the server default.
	PerPage int
}

ContactListParams filters a contact listing. Zero-valued fields are omitted.

type ContactPage

type ContactPage struct {
	// Data holds the contacts on this page.
	Data []Contact `json:"data"`
	// Total is the number of contacts matching the filters, across all pages.
	Total int64 `json:"total"`
	// Page is the 1-based number of this page.
	Page int64 `json:"page"`
	// PerPage is the page size the server actually applied, clamped to 100.
	PerPage int64 `json:"per_page"`
	// TotalPages is the number of pages at this page size.
	TotalPages int64 `json:"total_pages"`
}

ContactPage is one page of a contact listing, the pagination envelope the contacts endpoint wraps its rows in. Unlike conversations, which come back as a bare JSON array, contacts always arrive inside this envelope.

type ContactsService

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

ContactsService accesses the contact endpoints.

func (*ContactsService) All

All iterates every contact matching params, fetching pages lazily.

Contacts need their own iterator rather than the generic Paginate because the endpoint returns a pagination envelope, not a bare JSON array. The semantics are the same: the error value is non-nil at most once and ends iteration, and breaking out of the loop fetches no further pages.

params.PerPage sets the page size, clamped to 100; the page number in params is ignored, since iteration starts from the first page.

func (*ContactsService) List

func (s *ContactsService) List(ctx context.Context, params *ContactListParams) ([]Contact, error)

List returns one page of contacts matching the filters.

The endpoint wraps its rows in a pagination envelope; List unwraps it so the surface matches ConversationsService.List. Use ListPage when the totals are needed, or All to iterate every page.

func (*ContactsService) ListPage

func (s *ContactsService) ListPage(ctx context.Context, params *ContactListParams) (*ContactPage, error)

ListPage returns one page of contacts together with the pagination totals, for callers that render page numbers or a result count.

type Conversation

type Conversation struct {
	ID                string             `json:"id"`
	ContactID         string             `json:"contact_id"`
	AssignedAgentID   *string            `json:"assigned_agent_id,omitempty"`
	AssignedAgentName *string            `json:"assigned_agent_name,omitempty"`
	Status            ConversationStatus `json:"status"`
	LastMessageAt     time.Time          `json:"last_message_at"`
	UnreadCount       int                `json:"unread_count"`
	CreatedAt         time.Time          `json:"created_at"`
	UpdatedAt         time.Time          `json:"updated_at"`
	ContactName       *string            `json:"contact_name,omitempty"`
	ContactPhone      *string            `json:"contact_phone,omitempty"`
	ContactChannel    string             `json:"contact_channel"`
	// Assignees lists every co-assignee; AssignedAgentID remains the
	// denormalised primary.
	Assignees []Assignee `json:"assignees,omitempty"`
}

Conversation is a customer conversation with denormalised contact fields.

type ConversationListParams

type ConversationListParams struct {
	// Status filters by lifecycle state.
	Status ConversationStatus
	// AssignedAgentID filters to one agent's conversations.
	AssignedAgentID string
	// Page is 1-based. Zero means the server default.
	Page int
	// PerPage is capped at 100 by the server. Zero means the server default.
	PerPage int
}

ConversationListParams filters a conversation listing. Zero-valued fields are omitted from the request.

type ConversationStatus

type ConversationStatus string

ConversationStatus is the lifecycle state of a conversation.

const (
	// ConversationOpen is active and visible in the inbox.
	ConversationOpen ConversationStatus = "open"
	// ConversationSnoozed is temporarily hidden, and reopens by itself once
	// its snooze deadline passes.
	ConversationSnoozed ConversationStatus = "snoozed"
	// ConversationResolved is closed.
	ConversationResolved ConversationStatus = "resolved"
)

Conversation statuses. These are the only three the server accepts; filtering on anything else is rejected with a 400 before the handler runs.

type ConversationsService

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

ConversationsService accesses the conversation endpoints.

func (*ConversationsService) All

All iterates every conversation matching params, fetching pages lazily.

params.PerPage sets the page size, clamped to 100; the page number in params is ignored, since iteration starts from the first page.

func (*ConversationsService) Get

Get returns a single conversation by ID.

func (*ConversationsService) List

List returns conversations visible to the calling key. Agents see their own and unassigned conversations; supervisors and admins see all.

func (*ConversationsService) Messages

Messages returns one cursor-paginated page of a conversation's messages, oldest first within the page. Follow NextCursor to walk further back in time.

func (*ConversationsService) SendMessage

func (s *ConversationsService) SendMessage(ctx context.Context, id string, req SendMessageRequest, opts ...RequestOption) (*Message, error)

SendMessage queues an outbound message. Pass WithIdempotencyKey so a transient failure can be retried without sending twice.

type CreateAPIKeyRequest

type CreateAPIKeyRequest struct {
	Name string `json:"name"`
	// KeyType is "rest" or "webhook_signing".
	KeyType    string     `json:"key_type"`
	ExpiresAt  *time.Time `json:"expires_at,omitempty"`
	AllowedIPs []string   `json:"allowed_ips,omitempty"`
}

CreateAPIKeyRequest is the body for creating an API key.

type Message

type Message struct {
	// ID is the MongoDB ObjectId as a hex string.
	//
	// On the wire it arrives as MongoDB extended JSON — `{"$oid": "<hex>"}` —
	// because the server serialises a bson ObjectId directly. Message's
	// UnmarshalJSON flattens that, so callers only ever see the hex.
	ID             string           `json:"_id,omitempty"`
	ConversationID string           `json:"conversation_id"`
	ExternalID     *string          `json:"external_id,omitempty"`
	Direction      MessageDirection `json:"direction"`
	Type           MessageType      `json:"type"`
	// Content is free-form JSON whose shape depends on Type. Use Text for
	// text messages.
	Content     json.RawMessage `json:"content"`
	Status      MessageStatus   `json:"status"`
	SenderPhone *string         `json:"sender_phone,omitempty"`
	// SentByAgentID identifies the agent who sent an outbound message.
	SentByAgentID *string `json:"sent_by_agent_id,omitempty"`
	// SentByAgentName is that agent's display name.
	SentByAgentName *string `json:"sent_by_agent_name,omitempty"`
	// ErrorMessage is set only when Status is StatusFailed.
	ErrorMessage *string         `json:"error_message,omitempty"`
	ErrorCode    *string         `json:"error_code,omitempty"`
	ErrorSource  *string         `json:"error_source,omitempty"`
	ErrorDetails json.RawMessage `json:"error_details,omitempty"`
	CreatedAt    time.Time       `json:"created_at"`
}

Message is a single chat message.

func (Message) Text

func (m Message) Text() (string, bool)

Text decodes the message content as a text payload, returning false when the content is absent, is not JSON, or carries no text.

It accepts both keys the platform uses. Outbound sends built by TextMessage carry "text", which the gateway prefers; every message the API returns carries "body", because that is what the channel engines normalise inbound and outbound content to before storing it. A reader that only looked for "text" would return false for every stored message.

func (*Message) UnmarshalJSON

func (m *Message) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a message, accepting either form of the `_id` field.

The server emits a bson ObjectId as MongoDB extended JSON, `{"$oid": "<hex>"}`, on the conversation-messages endpoint, while the search endpoint emits the bare hex string. Decoding `_id` straight into a Go string fails on the former with "cannot unmarshal object into ... of type string", so both shapes are handled here and flattened to the hex.

type MessageDirection

type MessageDirection string

MessageDirection reports whether a message came from the customer or the business.

const (
	DirectionInbound  MessageDirection = "inbound"
	DirectionOutbound MessageDirection = "outbound"
)

Message directions.

type MessageListParams

type MessageListParams struct {
	// Cursor is the next_cursor from the previous page.
	Cursor string
	// Limit is capped at 100 by the server. Zero means the server default.
	Limit int
}

MessageListParams pages backwards through a conversation's messages: each page is older than the one before, while the messages inside a page are in ascending chronological order.

type MessageListResponse

type MessageListResponse struct {
	Messages []Message `json:"messages"`
	// NextCursor is a MongoDB ObjectId hex string, nil on the last page.
	NextCursor *string `json:"next_cursor,omitempty"`
	HasMore    bool    `json:"has_more"`
}

MessageListResponse is one cursor-paginated page of messages.

The two orderings differ, which is easy to get wrong: within a page, Messages is in ascending chronological order, oldest first. Successive pages walk backwards in time, each one older than the last — following NextCursor loads older history, the way a chat transcript scrolls up.

type MessageSearchPage

type MessageSearchPage struct {
	// Results holds the hits on this page.
	Results []MessageSearchResult `json:"results"`
	// Total is the number of matching messages across all pages.
	Total int64 `json:"total"`
	// Page is the 1-based number of this page.
	Page int64 `json:"page"`
	// PerPage is the page size the server actually applied, clamped to 50.
	PerPage int64 `json:"per_page"`
	// TotalPages is the number of pages at this page size.
	TotalPages int64 `json:"total_pages"`
}

MessageSearchPage is one page of search hits with its pagination totals.

type MessageSearchParams

type MessageSearchParams struct {
	// Query is the text matched against message content. Required, and must
	// contain more than whitespace.
	Query string
	// ConversationID scopes the search to a single conversation.
	ConversationID string
	// Channel filters by "whatsapp", "instagram", "email", or "messenger".
	Channel string
	// Direction filters to inbound or outbound messages.
	Direction MessageDirection
	// From is the ISO 8601 start of a created-at range filter.
	From string
	// To is the ISO 8601 end of a created-at range filter.
	To string
	// Page is 1-based. Zero means the server default of 1.
	Page int
	// PerPage is clamped to 1..50 by the server. Zero means its default of 20.
	PerPage int
}

MessageSearchParams filters a full-text message search. Query is required; every other zero-valued field is omitted from the request.

type MessageSearchResult

type MessageSearchResult struct {
	// ID is the MongoDB ObjectId hex string.
	ID             string           `json:"_id"`
	ConversationID string           `json:"conversation_id"`
	Direction      MessageDirection `json:"direction"`
	Type           MessageType      `json:"type"`
	// Content is free-form JSON whose shape depends on Type.
	Content json.RawMessage `json:"content"`
	// Snippet is the matched text wrapped in <mark> tags by the server. It is
	// an HTML fragment built from customer-supplied content — escape
	// everything outside the marks before rendering it.
	Snippet   string    `json:"snippet"`
	CreatedAt time.Time `json:"created_at"`
	// ContactName is the display name from the associated conversation.
	ContactName *string `json:"contact_name,omitempty"`
	// ContactPhone is the phone number from the associated conversation.
	ContactPhone *string `json:"contact_phone,omitempty"`
	// Channel is "whatsapp", "instagram", "email", or "messenger".
	Channel *string `json:"channel,omitempty"`
}

MessageSearchResult is one hit from a cross-conversation message search.

It is deliberately not a Message: the search endpoint returns a highlighted snippet and denormalised contact context, and carries no delivery status, provider ID, or failure detail. Fetch the conversation's messages if those are needed.

type MessageStatus

type MessageStatus string

MessageStatus is the delivery state of a message.

const (
	StatusPending   MessageStatus = "pending"
	StatusSent      MessageStatus = "sent"
	StatusDelivered MessageStatus = "delivered"
	StatusRead      MessageStatus = "read"
	StatusFailed    MessageStatus = "failed"
)

Message delivery statuses.

type MessageType

type MessageType string

MessageType is the media kind of a message.

const (
	MessageTypeText     MessageType = "text"
	MessageTypeImage    MessageType = "image"
	MessageTypeDocument MessageType = "document"
	MessageTypeTemplate MessageType = "template"
	MessageTypeAudio    MessageType = "audio"
	MessageTypeVideo    MessageType = "video"
	MessageTypeLocation MessageType = "location"
	MessageTypeSticker  MessageType = "sticker"
	// MessageTypeContacts is a shared contact card.
	MessageTypeContacts MessageType = "contacts"
	// MessageTypePostback is a button or quick-reply tap.
	MessageTypePostback MessageType = "postback"
	MessageTypeUnknown  MessageType = "unknown"
)

Message types.

type MessagesService

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

MessagesService accesses message endpoints that span conversations.

func (*MessagesService) Search

Search runs a full-text search over message content across conversations, returning one page of hits with the totals.

params must be non-nil and carry a Query with at least one non-whitespace character: the server trims the query and answers a blank one with an empty page, so the SDK rejects it rather than spending a round trip on it.

type NetworkError

type NetworkError struct {
	// Op describes the request that failed, e.g. "GET /api/contacts".
	Op string
	// TimedOut reports whether the failure was a deadline expiry rather than
	// a transport error.
	TimedOut bool
	// Err is the underlying cause.
	Err error
}

NetworkError is returned when a request never produced an HTTP response — a dial failure, a timeout, or a cancelled context.

func (*NetworkError) Error

func (e *NetworkError) Error() string

Error implements the error interface.

func (*NetworkError) Unwrap

func (e *NetworkError) Unwrap() error

Unwrap exposes the underlying cause to errors.Is and errors.As.

type Option

type Option func(*Client) error

Option configures a Client during construction.

func WithBaseURL

func WithBaseURL(raw string) Option

WithBaseURL sets the API gateway address. Trailing slashes are trimmed.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient supplies the underlying HTTP client, for custom transports, proxies, or tracing.

A Timeout set on the supplied client stays in force: net/http enforces it itself, per attempt, alongside the deadline from WithTimeout, and whichever is shorter ends the attempt. Either way the failure surfaces as a *NetworkError with TimedOut set.

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries sets how many retries follow a failed attempt. Zero disables retries.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout bounds each individual HTTP attempt. It is not a budget for the whole call: a retried request gets a fresh timeout per attempt. Use a context deadline to bound the total.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent appends an application identifier to the SDK's own User-Agent.

type RequestOption

type RequestOption func(*requestConfig)

RequestOption customises a single request.

func WithIdempotencyKey

func WithIdempotencyKey(key string) RequestOption

WithIdempotencyKey attaches an Idempotency-Key header. It also makes the request retryable: without a key, a non-GET request is never retried, because a retry could duplicate the effect.

func WithRequestRetries

func WithRequestRetries(n int) RequestOption

WithRequestRetries overrides the client's retry count for one request. Zero disables retries for that request.

A negative n is clamped to zero rather than rejected, as WithMaxRetries rejects it: a RequestOption has no way to report an error, and silently treating a negative count as "no retries" is the reading a caller expects.

type SendMessageRequest

type SendMessageRequest struct {
	Type MessageType `json:"type"`
	// Content is free-form JSON matching Type. Build it with TextMessage or
	// ImageMessage, or marshal your own.
	Content json.RawMessage `json:"content"`
}

SendMessageRequest is the body of an outbound send.

func ImageMessage

func ImageMessage(url, caption string) SendMessageRequest

ImageMessage builds an image send request. An empty caption is omitted.

func TextMessage

func TextMessage(text string) SendMessageRequest

TextMessage builds a plain-text send request.

type TemplateListParams

type TemplateListParams struct {
	// Status is APPROVED, PENDING, REJECTED, PAUSED, or DISABLED.
	Status string
	// Category is MARKETING, UTILITY, or AUTHENTICATION.
	Category string
	// WabaID filters to one WhatsApp Business Account.
	WabaID string
}

TemplateListParams filters a template listing. Zero-valued fields are omitted.

type TemplatesService

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

TemplatesService accesses the WhatsApp template endpoints.

func (*TemplatesService) List

List returns WhatsApp message templates matching the filters.

type WaTemplate

type WaTemplate struct {
	ID             string  `json:"id"`
	MetaTemplateID *string `json:"meta_template_id,omitempty"`
	// WabaID is the WhatsApp Business Account ID.
	WabaID string `json:"waba_id"`
	Name   string `json:"name"`
	// Language is a BCP-47 code such as "id" or "en_US".
	Language string `json:"language"`
	// Category is MARKETING, UTILITY, or AUTHENTICATION.
	Category string `json:"category"`
	// Status is APPROVED, PENDING, REJECTED, PAUSED, or DISABLED.
	Status string `json:"status"`
	// Components is Meta's component array (HEADER, BODY, FOOTER, BUTTONS).
	Components     json.RawMessage `json:"components"`
	HeaderMediaURL *string         `json:"header_media_url,omitempty"`
	CreatedAt      time.Time       `json:"created_at"`
	UpdatedAt      time.Time       `json:"updated_at"`
}

WaTemplate is a WhatsApp message template.

Directories

Path Synopsis
examples
receive_webhook command
Command receive_webhook serves an OmniStream webhook endpoint that logs inbound customer messages and failed deliveries.
Command receive_webhook serves an OmniStream webhook endpoint that logs inbound customer messages and failed deliveries.
send_message command
Command send_message sends a WhatsApp text message to the first open conversation, retrying safely with an idempotency key.
Command send_message sends a WhatsApp text message to the first open conversation, retrying safely with an idempotency key.
smoke command
Command smoke exercises every typed service of the OmniStream Go SDK against a live API and asserts on DECODED FIELD VALUES, not on err == nil.
Command smoke exercises every typed service of the OmniStream Go SDK against a live API and asserts on DECODED FIELD VALUES, not on err == nil.
Package webhook verifies and dispatches OmniStream outgoing webhooks.
Package webhook verifies and dispatches OmniStream outgoing webhooks.

Jump to

Keyboard shortcuts

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